diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 279473110..6c10d68b2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,6 +2,12 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.2.0 hooks: + - id: check-added-large-files + - id: check-case-conflict + - id: check-executables-have-shebangs + - id: check-merge-conflict + - id: check-shebang-scripts-are-executable + - id: check-symlinks - id: check-yaml - id: end-of-file-fixer - id: trailing-whitespace @@ -17,3 +23,21 @@ repos: hooks: - id: black args: [--line-length=79] +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.5.1 + hooks: + - id: mypy + args: [--ignore-missing-imports, --install-types, --non-interactive, --strict] + exclude: "/tests/.*\\.py|clean_example_notebooks.py|update_json_stubs_sitemap.py" +- repo: https://github.com/PyCQA/flake8 + rev: 6.1.0 + hooks: + - id: flake8 + additional_dependencies: [ + 'flake8-simplify', + 'flake8-comprehensions', + 'flake8-bugbear', + 'darglint', + 'flake8-pep585', + 'Flake8-pyproject', + ] diff --git a/doc/source/conf.py b/doc/source/conf.py index 3b30a2e54..c5ffa9b01 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -23,7 +23,7 @@ project = "MDAnalysis User Guide" -def sort_authors(filename): +def sort_authors(filename: str) -> list[str]: """Generate sorted list of authors from AUTHORS""" authors = [] with open(filename, "r") as f: @@ -144,7 +144,7 @@ def sort_authors(filename): # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] -html_css_files = [] +html_css_files: list[str] = [] # Custom sidebar templates, maps document names to template names. # alabaster sidebars @@ -170,8 +170,9 @@ def sort_authors(filename): } # nbsphinx -html_js_files = [ - # "https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.4/require.min.js", +html_js_files: list[str] = [ + # 'https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.4/require.min.js', + # DEFAULT_EMBED_REQUIREJS_URL, ] ipython_warning_is_error = False diff --git a/doc/source/scripts/.coverage b/doc/source/scripts/.coverage new file mode 100644 index 000000000..94227d533 Binary files /dev/null and b/doc/source/scripts/.coverage differ diff --git a/doc/source/scripts/base.py b/doc/source/scripts/base.py index d1da2a533..18b68e6c5 100644 --- a/doc/source/scripts/base.py +++ b/doc/source/scripts/base.py @@ -1,15 +1,65 @@ -from __future__ import print_function - import os import pathlib import sys import textwrap -from collections import defaultdict +from collections.abc import Callable, Iterable +from typing import Any, Optional, Type +import pandas as pd import tabulate -class TableWriter(object): +def _run_method(method: Callable[..., str], *args: Any) -> str: + val = method(*args) + return val + + +def _generate_row( + *, column_spec: list[tuple[str, Callable[..., str]]], args: Iterable[Any] +) -> dict[str, str]: + row = {} + for heading, method in column_spec: + val = _run_method(method, *args) + row[heading] = val + return row + + +def _generate_table( + *, + input_items: Iterable[Any], + column_spec: list[tuple[str, Callable[..., str]]], +) -> pd.DataFrame: + rows = [] + for args in input_items: + if not isinstance(args, Iterable): + args = [args] + line = _generate_row(column_spec=column_spec, args=args) + rows.append(line) + df = pd.DataFrame(rows) + return df + + +def write_table( + *, + path: str, + headers: list[str], + lines: list[list[str]], + include_table: Optional[str] = None, +) -> None: + parent_directory = pathlib.Path(path).parent + parent_directory.mkdir(exist_ok=True, parents=True) + with open(path, "w") as f: + f.write(f'..\n Generated by {sys.argv[0].split("/")[-1]}\n\n') + if include_table: + f.write(f".. table:: {include_table}\n\n") + tabled = tabulate.tabulate(lines, headers=headers, tablefmt="rst") + if include_table: + tabled = textwrap.indent(tabled, " ") + f.write(tabled) + print("Wrote ", path) + + +class TableWriter: """ For writing tables with easy column switching. @@ -18,91 +68,77 @@ class TableWriter(object): Filename relative to source. """ - filename = "" - include_table = False - headings = [] - preprocess = [] - postprocess = [] - sort = True + def __init__( + self, + column_spec: list[tuple[str, Callable[..., str]]], + lines: list[list[str]], + filename: str = "", + include_table: Optional[str] = None, + sort: bool = True, + input_items: Optional[Iterable[Any]] = None, + ): + if column_spec: + assert input_items + assert (column_spec and not lines) or (lines and not column_spec) + stem = os.getcwd().split("source")[0] + self.path = os.path.join(stem, "source", filename) + self.filename = filename + self.include_table = include_table + self.sort = sort + self.input_items = input_items + self.column_spec = column_spec + self.lines = lines + self._df = pd.DataFrame() - def __getattr__(self, key: str) -> list: - return self.fields[key] + @property + def headers(self) -> list[str]: + return [column_name for column_name, _ in self.column_spec] - def __init__(self, *args, **kwargs): - stem = os.getcwd().split("source")[0] - self.path = os.path.join(stem, "source", self.filename) - self.fields = defaultdict(list) + @property + def fields(self) -> pd.DataFrame: + return self._df - parent_directory = pathlib.Path(self.path).parent - parent_directory.mkdir(exist_ok=True, parents=True) - self.get_lines(*args, **kwargs) - self.write_table() + def generate_lines_and_write_table(self) -> None: + df = _generate_table( + input_items=self.input_items or [], + column_spec=self.column_spec, + ) - def _run_method(self, method, *args, **kwargs): - sanitized = self.sanitize_name(method) - meth = getattr(self, sanitized) - val = meth(*args, **kwargs) - self.fields[method].append(val) - return val - - @staticmethod - def sanitize_name(name): - return "_" + name.replace(" ", "_").replace("/", "_").lower() - - def get_lines(self, *args, **kwargs): - lines = [] - for items in self._set_up_input(): - try: - lines.append(self.get_line(*items)) - except TypeError: # one argument - lines.append(self.get_line(items)) - if self.sort: - lines = sorted(lines) + lines = df.values.tolist() + lines = sorted(lines) if self.sort else lines self.lines = lines + self._df = df + self.write_table() - def get_line(self, *args): - line = [] - for p in self.preprocess: - self._run_method(p, *args) - for h in self.headings: - line.append(self._run_method(h, *args)) - for p in self.postprocess: - self._run_method(p, *args) - return line - - def write_table(self): - with open(self.path, "w") as f: - f.write(f'..\n Generated by {sys.argv[0].split("/")[-1]}\n\n') - if self.include_table: - f.write(f".. table:: {self.include_table}\n\n") - tabled = tabulate.tabulate( - self.lines, headers=self.headings, tablefmt="rst" - ) - if self.include_table: - tabled = textwrap.indent(tabled, " ") - f.write(tabled) - print("Wrote ", self.filename) - - # ==== HELPER FUNCTIONS ==== # - - @staticmethod - def sphinx_class(klass, tilde=True): - prefix = "~" if tilde else "" - return ":class:`{}{}.{}`".format( - prefix, klass.__module__, klass.__name__ + def write_table(self) -> None: + write_table( + path=self.path, + headers=self.headers, + lines=self.lines, + include_table=self.include_table, ) - @staticmethod - def sphinx_meth(meth, tilde=True): - prefix = "~" if tilde else "" - return ":meth:`{}{}.{}`".format( - prefix, meth.__module__, meth.__qualname__ - ) - @staticmethod - def sphinx_ref(txt: str, label: str = None, suffix: str = "") -> str: - return f":ref:`{txt} <{label}{suffix}>`" +# ==== HELPER FUNCTIONS ==== # + + +def sphinx_class(*, klass: Type[Any], tilde: bool = True) -> str: + prefix = "~" if tilde else "" + return f":class:`{prefix}{klass.__module__}.{klass.__name__}`" + + +def sphinx_method(*, method: Callable[..., Any], tilde: bool = True) -> str: + prefix = "~" if tilde else "" + return ":meth:`{}{}.{}`".format( + prefix, method.__module__, method.__qualname__ + ) + + +def sphinx_ref( + *, txt: str, label: Optional[str] = None, suffix: str = "" +) -> str: + return f":ref:`{txt} <{label}{suffix}>`" + - @staticmethod - def sphinx_link(txt): - return "`{}`_".format(txt) +def sphinx_link(*, txt: str) -> str: + return f"`{txt}`_" diff --git a/doc/source/scripts/core.py b/doc/source/scripts/core.py index 59341d88e..0867b6c31 100644 --- a/doc/source/scripts/core.py +++ b/doc/source/scripts/core.py @@ -1,8 +1,3 @@ -from __future__ import print_function - -import os - -import tabulate from MDAnalysis import _TOPOLOGY_ATTRS # ====== TOPOLOGY ====== # @@ -75,11 +70,9 @@ for c in _TOPOLOGY_ATTRS.values() } -base_attrnames = set( - ["atomattrs", "residueattrs", "segmentattrs", "topologyattrs"] -) +base_attrnames = {"atomattrs", "residueattrs", "segmentattrs", "topologyattrs"} -core_attrnames = set(["indices", "resindices", "segindices"]) +core_attrnames = {"indices", "resindices", "segindices"} BASE_ATTRS = {k: v for k, v in ATTRS.items() if k in base_attrnames} @@ -90,12 +83,6 @@ } TOPOLOGY_CLS = sorted( - set( - [ - x - for x in _TOPOLOGY_ATTRS.values() - if x.attrname in NON_CORE_ATTRS.keys() - ] - ), + {x for x in _TOPOLOGY_ATTRS.values() if x.attrname in NON_CORE_ATTRS}, key=lambda x: x.attrname, ) diff --git a/doc/source/scripts/gen_format_overview_classes.py b/doc/source/scripts/gen_format_overview_classes.py index 9c03d44e3..e733f0a9b 100644 --- a/doc/source/scripts/gen_format_overview_classes.py +++ b/doc/source/scripts/gen_format_overview_classes.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python """ Generates: - ../formats/format_overview.txt : table of format overview @@ -7,124 +6,181 @@ """ from collections import defaultdict +from collections.abc import Iterable +from typing import Any, Final, Literal, Type +import base from base import TableWriter from core import DESCRIPTIONS from MDAnalysis import _CONVERTERS, _PARSERS, _READERS, _SINGLEFRAME_WRITERS -FILE_TYPES = defaultdict(dict) +HANDLER_T = Literal[ + "Coordinate reader", "Coordinate writer", "Topology parser", "Converter" +] -for clstype, dct in ( +PAIR_T = tuple[HANDLER_T, dict[str, Any]] + +handlers: Iterable[PAIR_T] = [ ("Coordinate reader", _READERS), ("Coordinate writer", _SINGLEFRAME_WRITERS), ("Topology parser", _PARSERS), ("Converter", _CONVERTERS), -): - for fmt, klass in dct.items(): - if fmt in ("CHAIN", "MEMORY", "MINIMAL", "NULL"): +] + +FILE_TYPES: dict[str, dict[HANDLER_T, Any]] = defaultdict(dict) + +for handler, dct in handlers: + for format, klass in dct.items(): + if format in {"CHAIN", "MEMORY", "MINIMAL", "NULL"}: continue # get their own pages - FILE_TYPES[fmt][clstype] = klass + FILE_TYPES[format][handler] = klass -sorted_types = sorted(FILE_TYPES.items()) + +SORTED_FILE_TYPES: Final = sorted(FILE_TYPES.items()) SUCCESS = "\u2713" # checkmark FAIL = "" -class FormatOverview(TableWriter): - filename = "formats/format_overview.txt" - include_table = "Table of all supported formats in MDAnalysis" - preprocess = ["keys"] - headings = [ - "File type", - "Description", - "Topology", - "Coordinates", - "Read", - "Write", - ] - - def _set_up_input(self): - return sorted_types - - def _file_type(self, fmt, handlers): - return self.sphinx_ref(fmt, self.keys[-1], suffix="-format") +def _create_key(fmt: str, handlers: dict[HANDLER_T, Type[Any]]) -> str: + if fmt in DESCRIPTIONS: + key = fmt + else: + key = list(handlers.values())[0].format[0] - def _keys(self, fmt, handlers): - if fmt in DESCRIPTIONS: + # raise an informative error + if key not in DESCRIPTIONS: key = fmt - else: - key = list(handlers.values())[0].format[0] + return key - # raise an informative error - if key not in DESCRIPTIONS: - key = fmt - return key - def _description(self, fmt, handlers): - return DESCRIPTIONS[self.keys[-1]] +def _file_type( + fmt: str, handlers: dict[HANDLER_T, Type[Any]], key: str +) -> str: + return base.sphinx_ref(txt=fmt, label=key, suffix="-format") - def _topology(self, fmt, handlers): - if "Topology parser" in handlers: - return SUCCESS - return FAIL - - def _coordinates(self, fmt, handlers): - if "Coordinate reader" in handlers: - return SUCCESS - return FAIL - def _read(self, fmt, handlers): - return SUCCESS +def _description( + fmt: str, handlers: dict[HANDLER_T, Type[Any]], key: str +) -> str: + return DESCRIPTIONS[key] - def _write(self, fmt, handlers): - if "Coordinate writer" in handlers: - return SUCCESS - if "Converter" in handlers: - return SUCCESS - return FAIL +class FormatOverview: + def __init__(self) -> None: + def _topology( + fmt: str, handlers: dict[HANDLER_T, Type[Any]], key: str + ) -> str: + return SUCCESS if "Topology parser" in handlers else FAIL -class CoordinateReaders(FormatOverview): - filename = "formats/coordinate_readers.txt" - include_table = ( - "Table of supported coordinate readers and the information read" - ) - headings = ["File type", "Description", "Velocities", "Forces"] + def _coordinates( + fmt: str, handlers: dict[HANDLER_T, Type[Any]], key: str + ) -> str: + if "Coordinate reader" in handlers: + return SUCCESS + return FAIL - def _set_up_input(self): - return [(x, y) for x, y in sorted_types if "Coordinate reader" in y] - - def _velocities(self, fmt, handlers): - if handlers["Coordinate reader"].units.get("velocity", None): - return SUCCESS - return FAIL - - def _forces(self, fmt, handlers): - if handlers["Coordinate reader"].units.get("force", None): + def _read( + fmt: str, handlers: dict[HANDLER_T, Type[Any]], key: str + ) -> str: return SUCCESS - return FAIL - - -class SphinxClasses(TableWriter): - filename = "formats/reference/classes/{}.txt" - def __init__(self, fmt): - self.filename = self.filename.format(fmt) - self.fmt = fmt - super(SphinxClasses, self).__init__() - - def get_lines(self): - lines = [] - for label, klass in sorted(FILE_TYPES[self.fmt].items()): - lines.append( - ["**{}**".format(label), self.sphinx_class(klass, tilde=False)] - ) - self.lines = lines + def _write( + fmt: str, handlers: dict[HANDLER_T, Type[Any]], key: str + ) -> str: + if "Coordinate writer" in handlers: + return SUCCESS + if "Converter" in handlers: + return SUCCESS + return FAIL + + input_items = [ + (format, handlers, _create_key(format, handlers)) + for format, handlers in SORTED_FILE_TYPES + ] + + self.table_writer = TableWriter( + filename="formats/format_overview.txt", + include_table="Table of all supported formats in MDAnalysis", + column_spec=[ + ("File type", _file_type), + ("Description", _description), + ("Topology", _topology), + ("Coordinates", _coordinates), + ("Read", _read), + ("Write", _write), + ], + input_items=input_items, + lines=[], + ) + self.table_writer.generate_lines_and_write_table() + self.table_writer.fields["keys"] = list(zip(*input_items))[2] + + +class CoordinateReaders: + def __init__(self) -> None: + def _velocities( + fmt: str, handlers: dict[HANDLER_T, Type[Any]], key: str + ) -> str: + if handlers["Coordinate reader"].units.get("velocity"): + return SUCCESS + return FAIL + + def _forces( + fmt: str, handlers: dict[HANDLER_T, Type[Any]], key: str + ) -> str: + if handlers["Coordinate reader"].units.get("force"): + return SUCCESS + return FAIL + + input_items = [ + (format, handlers, _create_key(format, handlers)) + for format, handlers in SORTED_FILE_TYPES + if "Coordinate reader" in handlers + ] + self.table_writer = TableWriter( + filename="formats/coordinate_readers.txt", + include_table="Table of supported coordinate readers and the information read", + input_items=input_items, + column_spec=[ + ("File type", _file_type), + ("Description", _description), + ("Velocities", _velocities), + ("Forces", _forces), + ], + lines=[], + ) + self.table_writer.generate_lines_and_write_table() + + +class SphinxClasses: + def __init__(self, fmt: str): + def _custom_get_lines() -> list[list[str]]: + lines = [] + for label, klass in sorted(FILE_TYPES[fmt].items()): + lines.append( + [ + f"**{label}**", + base.sphinx_class(klass=klass, tilde=False), + ] + ) + return lines + + lines = _custom_get_lines() + self.table_writer = TableWriter( + filename=f"formats/reference/classes/{fmt}.txt", + lines=lines, + column_spec=[], + ) + self.table_writer.write_table() + + +def main() -> None: + overview = FormatOverview() + CoordinateReaders() + for key in set(overview.table_writer.fields["keys"]): + SphinxClasses(key) if __name__ == "__main__": - ov = FormatOverview() - CoordinateReaders() - for key in set(ov.fields["keys"]): - SphinxClasses(key) + main() diff --git a/doc/source/scripts/gen_selection_exporters.py b/doc/source/scripts/gen_selection_exporters.py index f4ffbab04..76acd5762 100644 --- a/doc/source/scripts/gen_selection_exporters.py +++ b/doc/source/scripts/gen_selection_exporters.py @@ -1,11 +1,12 @@ -#!/usr/bin/env python """ Generates: - ../formats/selection_exporters.txt """ +import base from base import TableWriter from MDAnalysis import _SELECTION_WRITERS +from MDAnalysis.selections.base import SelectionWriterBase SELECTION_DESCRIPTIONS = { "vmd": "VMD macros, available in Representations", @@ -16,31 +17,39 @@ } -class SelectionExporterWriter(TableWriter): - headings = ["Program", "Extension", "Description", "Class"] - filename = "formats/selection_exporter_formats.txt" - include_table = "Supported selection exporters" - sort = True - - def _set_up_input(self): - return set(_SELECTION_WRITERS.values()) - - def _program(self, klass): - # classes have multiple formats. - # First tends to be the program name, second is extension - p = klass.format - if isinstance(p, (list, tuple)): - p = p[0] - return self.sphinx_link(p) - - def _extension(self, klass): - return klass.ext - - def _description(self, klass): - return SELECTION_DESCRIPTIONS[klass.ext] - - def _class(self, klass): - return self.sphinx_class(klass, tilde=False) +class SelectionExporterWriter: + def __init__(self) -> None: + def _program(klass: SelectionWriterBase) -> str: + # classes have multiple formats. + # First tends to be the program name, second is extension + p = klass.format + if isinstance(p, (list, tuple)): + p = p[0] + return base.sphinx_link(txt=p) + + def _extension(klass: SelectionWriterBase) -> str: + return klass.ext # type: ignore + + def _description(klass: SelectionWriterBase) -> str: + return SELECTION_DESCRIPTIONS[klass.ext] + + def _class(klass: SelectionWriterBase) -> str: + return base.sphinx_class(klass=klass, tilde=False) + + self.table_writer = TableWriter( + filename="formats/selection_exporter_formats.txt", + include_table="Supported selection exporters", + sort=True, + input_items=set(_SELECTION_WRITERS.values()), + column_spec=[ + ("Program", _program), + ("Extension", _extension), + ("Description", _description), + ("Class", _class), + ], + lines=[], + ) + self.table_writer.generate_lines_and_write_table() if __name__ == "__main__": diff --git a/doc/source/scripts/gen_standard_selections.py b/doc/source/scripts/gen_standard_selections.py index a9304fbc0..c54b0c7f4 100755 --- a/doc/source/scripts/gen_standard_selections.py +++ b/doc/source/scripts/gen_standard_selections.py @@ -8,42 +8,70 @@ - nucleobase atoms - nucleic sugar atoms """ +from typing import Any + from base import TableWriter from MDAnalysis.core import selection as sel -def chunk_list(lst, n=8): - return [lst[i : i + n] for i in range(0, len(lst), n)] +def _chunk_list(lst: list[str], chunk_size: int = 8) -> list[list[str]]: + return [lst[i : i + chunk_size] for i in range(0, len(lst), chunk_size)] + +# override get_lines as there are no headings +def _custom_get_lines( + *, + klass: sel.Selection, + attribute_name: str, + sort: bool = False, + chunk_size: int = 8 +) -> list[list[str]]: + selected = getattr(klass, attribute_name) + if sort: + selected = sorted(selected) -class StandardSelectionTable(TableWriter): - sort = False - filename = "generated/selections/{}.txt" + table = _chunk_list(list(selected), chunk_size=chunk_size) + return table - def __init__(self, filename, *args, **kwargs): - self.filename = self.filename.format(filename) - super(StandardSelectionTable, self).__init__(*args, **kwargs) - # override get_lines as there are no headings - def get_lines(self, klass, attr, sort=False, n=8): - selected = getattr(klass, attr) - if sort: - selected = sorted(selected) +class StandardSelectionTable: + def __init__(self, filename: str, *args: Any, **kwargs: Any) -> None: - table = chunk_list(list(selected), n=n) - self.lines = table + lines = _custom_get_lines(*args, **kwargs) + self.table_writer = TableWriter( + sort=False, + filename="generated/selections/{}.txt".format(filename), + lines=lines, + column_spec=[], + ) + self.table_writer.write_table() if __name__ == "__main__": - StandardSelectionTable("protein", sel.ProteinSelection, "prot_res", True) StandardSelectionTable( - "protein_backbone", sel.BackboneSelection, "bb_atoms" + "protein", + klass=sel.ProteinSelection, + attribute_name="prot_res", + sort=True, + ) + StandardSelectionTable( + "protein_backbone", + klass=sel.BackboneSelection, + attribute_name="bb_atoms", + ) + StandardSelectionTable( + "nucleic", klass=sel.NucleicSelection, attribute_name="nucl_res" + ) + StandardSelectionTable( + "nucleic_backbone", + klass=sel.NucleicBackboneSelection, + attribute_name="bb_atoms", ) - StandardSelectionTable("nucleic", sel.NucleicSelection, "nucl_res") StandardSelectionTable( - "nucleic_backbone", sel.NucleicBackboneSelection, "bb_atoms" + "base", klass=sel.BaseSelection, attribute_name="base_atoms" ) - StandardSelectionTable("base", sel.BaseSelection, "base_atoms") StandardSelectionTable( - "nucleic_sugar", sel.NucleicSugarSelection, "sug_atoms" + "nucleic_sugar", + klass=sel.NucleicSugarSelection, + attribute_name="sug_atoms", ) diff --git a/doc/source/scripts/gen_topology_groupmethods.py b/doc/source/scripts/gen_topology_groupmethods.py index f6eb92ec9..b6b32cf9e 100644 --- a/doc/source/scripts/gen_topology_groupmethods.py +++ b/doc/source/scripts/gen_topology_groupmethods.py @@ -1,36 +1,56 @@ -#!/usr/bin/env python """ Generate groupmethods.txt: A table of transplanted methods. """ -from collections import defaultdict +from collections.abc import Callable +from typing import Any +import base from base import TableWriter from core import TOPOLOGY_CLS from MDAnalysis.core.groups import GroupBase - - -class TransplantedMethods(TableWriter): - headings = ["Method", "Description", "Requires"] - filename = "generated/topology/groupmethods.txt" - - def _set_up_input(self): - items = [] - for klass in TOPOLOGY_CLS: - for name, method in klass.transplants[GroupBase]: - items.append([name, klass, method]) - return [x[1:] for x in sorted(items)] - - def _method(self, klass, method): - return self.sphinx_meth(method) - - def _description(self, klass, method): - return " ".join(method.__doc__.split(".\n")[0].split()) - - def _requires(self, klass, method): - return klass.attrname +from MDAnalysis.core.topologyattrs import TopologyAttr + + +class TransplantedMethods: + def __init__(self) -> None: + def _generate_input_items() -> list[tuple[TopologyAttr, Any]]: + items = [] + for klass in TOPOLOGY_CLS: + for name, method in klass.transplants[GroupBase]: + items.append((name, klass, method)) + return [x[1:] for x in sorted(items)] + + def _method( + klass: TopologyAttr, method: Callable[[TopologyAttr], str] + ) -> str: + return base.sphinx_method(method=method) + + def _description( + klass: TopologyAttr, method: Callable[[TopologyAttr], str] + ) -> str: + assert method.__doc__ + return " ".join(method.__doc__.split(".\n")[0].split()) + + def _requires( + klass: TopologyAttr, method: Callable[[TopologyAttr], str] + ) -> str: + return klass.attrname # type: ignore + + input_items = _generate_input_items() + self.table_writer = TableWriter( + filename="generated/topology/groupmethods.txt", + input_items=input_items, + column_spec=[ + ("Method", _method), + ("Description", _description), + ("Requires", _requires), + ], + lines=[], + ) + self.table_writer.generate_lines_and_write_table() if __name__ == "__main__": diff --git a/doc/source/scripts/gen_topologyattr_defaults.py b/doc/source/scripts/gen_topologyattr_defaults.py index 1990ea6ce..4887cfa1d 100644 --- a/doc/source/scripts/gen_topologyattr_defaults.py +++ b/doc/source/scripts/gen_topologyattr_defaults.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python """ Generate topology_defaults.txt: @@ -7,7 +6,12 @@ from base import TableWriter from core import TOPOLOGY_CLS -from MDAnalysis.core.topologyattrs import AtomAttr, ResidueAttr, SegmentAttr +from MDAnalysis.core.topologyattrs import ( + AtomAttr, + ResidueAttr, + SegmentAttr, + TopologyAttr, +) DEFAULTS = { "resids": "continuous sequence from 1 to n_residues", @@ -16,42 +20,51 @@ } -class TopologyDefaults(TableWriter): - filename = "generated/topology/defaults.txt" - headings = ("Atom", "AtomGroup", "default", "level", "type") - sort = True +class TopologyDefaults: + def __init__(self) -> None: + def _atom(klass: TopologyAttr) -> str: + return klass.attrname # type: ignore - def _set_up_input(self): - return TOPOLOGY_CLS + def _atomgroup(klass: TopologyAttr) -> str: + return klass.singular # type: ignore - def _atom(self, klass): - return klass.attrname + def _default(klass: TopologyAttr) -> str: + try: + return DEFAULTS[klass.attrname] + except KeyError: + try: + return repr(klass._gen_initial_values(1, 1, 1)[0]) + except NotImplementedError: + return "No default values" - def _atomgroup(self, klass): - return klass.singular + def _level(klass: TopologyAttr) -> str: + if issubclass(klass, AtomAttr): + level = "atom" + elif issubclass(klass, ResidueAttr): + level = "residue" + elif issubclass(klass, SegmentAttr): + level = "segment" + else: + raise ValueError + return level - def _default(self, klass): - try: - return DEFAULTS[klass.attrname] - except KeyError: - try: - return repr(klass._gen_initial_values(1, 1, 1)[0]) - except NotImplementedError: - return "No default values" - - def _level(self, klass): - if issubclass(klass, AtomAttr): - level = "atom" - elif issubclass(klass, ResidueAttr): - level = "residue" - elif issubclass(klass, SegmentAttr): - level = "segment" - else: - raise ValueError - return level - - def _type(self, klass): - return klass.dtype + def _type(klass: TopologyAttr) -> str: + return klass.dtype # type: ignore + + self.table_writer = TableWriter( + filename="generated/topology/defaults.txt", + sort=True, + input_items=TOPOLOGY_CLS, + column_spec=[ + ("Atom", _atom), + ("AtomGroup", _atomgroup), + ("default", _default), + ("level", _level), + ("type", _type), + ], + lines=[], + ) + self.table_writer.generate_lines_and_write_table() if __name__ == "__main__": diff --git a/doc/source/scripts/gen_topologyparser_attrs.py b/doc/source/scripts/gen_topologyparser_attrs.py index e648fb737..ebeefbe76 100755 --- a/doc/source/scripts/gen_topologyparser_attrs.py +++ b/doc/source/scripts/gen_topologyparser_attrs.py @@ -7,12 +7,13 @@ This script imports the testsuite, which tests these. """ -import os -import sys from collections import defaultdict +from typing import Any +import base from base import TableWriter from core import DESCRIPTIONS, NON_CORE_ATTRS +from MDAnalysis.topology.base import TopologyReaderBase from MDAnalysisTests.topology.base import mandatory_attrs from MDAnalysisTests.topology.test_crd import TestCRDParser from MDAnalysisTests.topology.test_dlpoly import ( @@ -64,110 +65,165 @@ ) -MANDATORY_ATTRS = set(mandatory_attrs) - - -parser_attrs = {} - - -for p in PARSER_TESTS: - e, g = set(p.expected_attrs) - MANDATORY_ATTRS, set(p.guessed_attrs) - # clunky hack for PDB - if p is TestPDBParser: - e.add("elements") - parser_attrs[p.parser] = (e, g) - - -class TopologyParsers(TableWriter): - headings = [ - "Format", - "Description", - "Attributes read", - "Attributes guessed", - ] - preprocess = ["keys"] - filename = "formats/topology_parsers.txt" - include_table = ( - "Table of supported topology parsers and the attributes read" - ) - sort = True - - def __init__(self): - self.attrs = defaultdict(set) - super(TopologyParsers, self).__init__() - - def _set_up_input(self): - return [[x, *y] for x, y in parser_attrs.items()] - - def get_line(self, parser, expected, guessed): - line = super(TopologyParsers, self).get_line(parser, expected, guessed) - for a in expected | guessed: - self.attrs[a].add(self.fields["Format"][-1]) - return line - - def _keys(self, parser, *args): - f = parser.format - if isinstance(f, (list, tuple)): - key = f[0] - label = ", ".join(f) - else: - key = label = f - return (key, label) +def create_parser_attributes() -> dict[Any, tuple[set[str], set[str]]]: + parser_attrs = {} + for test_parser_class in PARSER_TESTS: + expected = set(test_parser_class.expected_attrs) - set(mandatory_attrs) + guessed = set(test_parser_class.guessed_attrs) + # clunky hack for PDB + if test_parser_class is TestPDBParser: + expected.add("elements") + parser_attrs[test_parser_class.parser] = (expected, guessed) + return parser_attrs + + +class TopologyParsers: + def __init__(self) -> None: + def _keys(parser: TopologyReaderBase) -> tuple[str, str]: + f = parser.format + if isinstance(f, (list, tuple)): + key = f[0] + label = ", ".join(f) + else: + key = label = f + return (key, label) + + def _description( + parser: TopologyReaderBase, + expected: set[str], + guessed: set[str], + key_label: tuple[str, str], + ) -> str: + key, label = key_label + return DESCRIPTIONS[key] + + def _format( + parser: TopologyReaderBase, + expected: set[str], + guessed: set[str], + key_label: tuple[str, str], + ) -> str: + key, label = key_label + return base.sphinx_ref(txt=label, label=key, suffix="-format") + + def _attributes_read( + parser: TopologyReaderBase, + expected: set[str], + guessed: set[str], + key_label: tuple[str, str], + ) -> str: + vals = sorted(expected - guessed) + return ", ".join(vals) + + def _attributes_guessed( + parser: TopologyReaderBase, + expected: set[str], + guessed: set[str], + key_label: tuple[str, str], + ) -> str: + return ", ".join(sorted(guessed)) + + parser_attrs = create_parser_attributes() + input_items = [ + [parser, expected, guessed, _keys(parser=parser)] + for parser, (expected, guessed) in parser_attrs.items() + ] + self.table_writer = TableWriter( + filename="formats/topology_parsers.txt", + include_table="Table of supported topology parsers and the attributes read", + sort=True, + input_items=input_items, + lines=[], + column_spec=[ + ("Format", _format), + ("Description", _description), + ("Attributes read", _attributes_read), + ("Attributes guessed", _attributes_guessed), + ], + ) + self.table_writer.generate_lines_and_write_table() - def _description(self, *args): - key, label = self.keys[-1] - return DESCRIPTIONS[key] - def _format(self, *args): - key, label = self.keys[-1] - return self.sphinx_ref(label, key, suffix="-format") +def get_format_attrs(topology_parsers: TopologyParsers) -> dict[str, set[str]]: + attrs = defaultdict(set) + writer = topology_parsers.table_writer + assert writer.input_items + for format, (_, expected, guessed, _) in zip( + writer.fields["Format"], + writer.input_items, + ): + for attribute in expected | guessed: + attrs[attribute].add(format) + return attrs - def _attributes_read(self, parser, expected, guessed): - vals = sorted(expected - guessed) - return ", ".join(vals) - def _attributes_guessed(self, parser, expected, guessed): - return ", ".join(sorted(guessed)) +class TopologyAttrs: + def __init__(self, attrs: dict[str, set[str]]) -> None: + def _atom(name: str, singular: str, description: str) -> str: + return singular + def _atomgroup(name: str, singular: str, description: str) -> str: + return name -class TopologyAttrs(TableWriter): - headings = ("Atom", "AtomGroup", "Description", "Supported formats") - filename = "generated/topology/topologyattrs.txt" + def _description(name: str, singular: str, description: str) -> str: + return description - def __init__(self, attrs): - self.attrs = attrs - super(TopologyAttrs, self).__init__() + def _supported_formats( + name: str, singular: str, description: str + ) -> str: + return ", ".join(sorted(attrs[name])) - def _set_up_input(self): - return sorted( + input_items = sorted( [x, *y] for x, y in NON_CORE_ATTRS.items() - if x not in MANDATORY_ATTRS + if x not in set(mandatory_attrs) + ) + self.table_writer = TableWriter( + filename="generated/topology/topologyattrs.txt", + lines=[], + input_items=input_items, + column_spec=[ + ("Atom", _atom), + ("AtomGroup", _atomgroup), + ("Description", _description), + ("Supported formats", _supported_formats), + ], ) + self.table_writer.generate_lines_and_write_table() - def _atom(self, name, singular, *args): - return singular - def _atomgroup(self, name, *args): - return name +class ConnectivityAttrs: + def __init__(self, attrs: dict[str, set[str]]) -> None: + def _atom(name: str) -> str: + return name - def _description(self, name, singular, description): - return description + def _atomgroup(name: str) -> str: + return name - def _supported_formats(self, name, singular, description): - return ", ".join(sorted(self.attrs[name])) + def _supported_formats(name: str) -> str: + return ", ".join(sorted(attrs[name])) + input_items = [("bonds",), ("angles",), ("dihedrals",), ("impropers",)] + + self.table_writer = TableWriter( + filename="generated/topology/connectivityattrs.txt", + input_items=input_items, + lines=[], + column_spec=[ + ("Atom", _atom), + ("AtomGroup", _atomgroup), + ("Supported formats", _supported_formats), + ], + ) + self.table_writer.generate_lines_and_write_table() -class ConnectivityAttrs(TopologyAttrs): - headings = ("Atom", "AtomGroup", "Supported formats") - filename = "generated/topology/connectivityattrs.txt" - def _set_up_input(self): - inp = [[x] * 3 for x in "bonds angles dihedrals impropers".split()] - return inp +def main() -> None: + top = TopologyParsers() + topology_attrs = get_format_attrs(top) + TopologyAttrs(topology_attrs) + ConnectivityAttrs(topology_attrs) if __name__ == "__main__": - top = TopologyParsers() - TopologyAttrs(top.attrs) - ConnectivityAttrs(top.attrs) + main() diff --git a/doc/source/scripts/gen_unit_tables.py b/doc/source/scripts/gen_unit_tables.py index c18ab5f4e..8fffdf7c6 100755 --- a/doc/source/scripts/gen_unit_tables.py +++ b/doc/source/scripts/gen_unit_tables.py @@ -10,19 +10,20 @@ from MDAnalysis.units import conversion_factor -def write_unit_table(filename): +def write_unit_table( + filename: str, +) -> list[tuple[str, list[tuple[str, float]]]]: headings = ["Unit", "Conversion factor"] - table_heading = ".. table:: {}" tables = [] for data_type, items in conversion_factor.items(): - lines = sorted(list(items.items())) + lines = sorted(items.items()) tables.append((data_type, lines)) parent_directory = pathlib.Path(__file__).parent.parent parent_directory.mkdir(exist_ok=True, parents=True) - filename = parent_directory / filename + output = parent_directory / filename - with filename.open("w") as f: + with output.open("w") as f: f.write(".. Generated by {}\n".format(sys.argv[0])) for data_type, lines in tables: line = "\n" + "-" * len(data_type) + "\n" diff --git a/doc/source/scripts/tests/snapshot/test_gen_format_overview_classes.py b/doc/source/scripts/tests/snapshot/test_gen_format_overview_classes.py index 835e5d039..647f4f89b 100644 --- a/doc/source/scripts/tests/snapshot/test_gen_format_overview_classes.py +++ b/doc/source/scripts/tests/snapshot/test_gen_format_overview_classes.py @@ -15,16 +15,16 @@ def test_FILE_TYPES(): def test_FormatOverview(snapshot): with patch("builtins.open"): ov = FormatOverview() - assert ov.lines == snapshot + assert ov.table_writer.lines == snapshot def test_CoordinateReaders(snapshot): with patch("builtins.open"): cr = CoordinateReaders() - assert cr.lines == snapshot + assert cr.table_writer.lines == snapshot def test_SphinxClasses(snapshot): with patch("builtins.open"): sc = SphinxClasses("PDB") - assert sc.lines == snapshot + assert sc.table_writer.lines == snapshot diff --git a/doc/source/scripts/tests/snapshot/test_gen_selection_exporters.py b/doc/source/scripts/tests/snapshot/test_gen_selection_exporters.py index 34d8a5616..fae8d5d68 100644 --- a/doc/source/scripts/tests/snapshot/test_gen_selection_exporters.py +++ b/doc/source/scripts/tests/snapshot/test_gen_selection_exporters.py @@ -1,10 +1,9 @@ from unittest.mock import patch from gen_selection_exporters import SelectionExporterWriter -from MDAnalysis.core import selection as sel def test_SelectionExporterWriter(snapshot): with patch("builtins.open"): se = SelectionExporterWriter() - assert se.lines == snapshot + assert se.table_writer.lines == snapshot diff --git a/doc/source/scripts/tests/snapshot/test_gen_standard_selections.py b/doc/source/scripts/tests/snapshot/test_gen_standard_selections.py index dc4529de3..ff47ea158 100644 --- a/doc/source/scripts/tests/snapshot/test_gen_standard_selections.py +++ b/doc/source/scripts/tests/snapshot/test_gen_standard_selections.py @@ -7,46 +7,64 @@ def test_StandardSelectionTable_protein(snapshot): with patch("builtins.open"): ss = StandardSelectionTable( - "protein", sel.ProteinSelection, "prot_res", True + "protein", + klass=sel.ProteinSelection, + attribute_name="prot_res", + sort=True, ) - assert ss.lines == snapshot + assert ss.table_writer.lines == snapshot def test_StandardSelectionTable_protein_backbone(snapshot): with patch("builtins.open"): ss = StandardSelectionTable( - "protein_backbone", sel.BackboneSelection, "bb_atoms", True + "protein_backbone", + klass=sel.BackboneSelection, + attribute_name="bb_atoms", + sort=True, ) - assert ss.lines == snapshot + assert ss.table_writer.lines == snapshot def test_StandardSelectionTable_nucleic(snapshot): with patch("builtins.open"): ss = StandardSelectionTable( - "nucleic", sel.NucleicSelection, "nucl_res", True + "nucleic", + klass=sel.NucleicSelection, + attribute_name="nucl_res", + sort=True, ) - assert ss.lines == snapshot + assert ss.table_writer.lines == snapshot def test_StandardSelectionTable_nucleic_backbone(snapshot): with patch("builtins.open"): ss = StandardSelectionTable( - "nucleic_backbone", sel.NucleicBackboneSelection, "bb_atoms", True + "nucleic_backbone", + klass=sel.NucleicBackboneSelection, + attribute_name="bb_atoms", + sort=True, ) - assert ss.lines == snapshot + assert ss.table_writer.lines == snapshot def test_StandardSelectionTable_base(snapshot): with patch("builtins.open"): ss = StandardSelectionTable( - "base", sel.BaseSelection, "base_atoms", True + "base", + klass=sel.BaseSelection, + attribute_name="base_atoms", + sort=True, ) - assert ss.lines == snapshot + assert ss.table_writer.lines == snapshot def test_StandardSelectionTable_nucleic_sugar(snapshot): with patch("builtins.open"): ss = StandardSelectionTable( - "nucleic_sugar", sel.NucleicSugarSelection, "sug_atoms", True + "nucleic_sugar", + klass=sel.NucleicSugarSelection, + attribute_name="sug_atoms", + sort=True, ) - assert ss.lines == snapshot + assert ss.table_writer.lines == snapshot diff --git a/doc/source/scripts/tests/snapshot/test_gen_topology_groupmethods.py b/doc/source/scripts/tests/snapshot/test_gen_topology_groupmethods.py index f585f489d..741bd1263 100644 --- a/doc/source/scripts/tests/snapshot/test_gen_topology_groupmethods.py +++ b/doc/source/scripts/tests/snapshot/test_gen_topology_groupmethods.py @@ -1,10 +1,9 @@ from unittest.mock import patch from gen_topology_groupmethods import TransplantedMethods -from MDAnalysis.core import selection as sel def test_TransplantedMethods(snapshot): with patch("builtins.open"): tm = TransplantedMethods() - assert tm.lines == snapshot + assert tm.table_writer.lines == snapshot diff --git a/doc/source/scripts/tests/snapshot/test_gen_topologyattr_defaults.py b/doc/source/scripts/tests/snapshot/test_gen_topologyattr_defaults.py index bf461d275..47b817b6d 100644 --- a/doc/source/scripts/tests/snapshot/test_gen_topologyattr_defaults.py +++ b/doc/source/scripts/tests/snapshot/test_gen_topologyattr_defaults.py @@ -1,10 +1,9 @@ from unittest.mock import patch from gen_topologyattr_defaults import TopologyDefaults -from MDAnalysis.core import selection as sel def test_TopologyDefaults(snapshot): with patch("builtins.open"): td = TopologyDefaults() - assert td.lines == snapshot + assert td.table_writer.lines == snapshot diff --git a/doc/source/scripts/tests/snapshot/test_gen_topologyparser_attrs.py b/doc/source/scripts/tests/snapshot/test_gen_topologyparser_attrs.py index f8d95a152..bf94a3aed 100644 --- a/doc/source/scripts/tests/snapshot/test_gen_topologyparser_attrs.py +++ b/doc/source/scripts/tests/snapshot/test_gen_topologyparser_attrs.py @@ -4,31 +4,34 @@ ConnectivityAttrs, TopologyAttrs, TopologyParsers, + get_format_attrs, ) -from MDAnalysis.core import selection as sel def test_TopologyParsers_lines(snapshot): with patch("builtins.open"): top = TopologyParsers() - assert top.lines == snapshot + assert top.table_writer.lines == snapshot def test_TopologyParsers_attrs(snapshot): with patch("builtins.open"): top = TopologyParsers() - assert top.attrs == snapshot + attrs = get_format_attrs(top) + assert attrs == snapshot def test_TopologyAttrs(snapshot): with patch("builtins.open"): top = TopologyParsers() - ta = TopologyAttrs(top.attrs) - assert ta.lines == snapshot + attrs = get_format_attrs(top) + ta = TopologyAttrs(attrs) + assert ta.table_writer.lines == snapshot def test_ConnectivityAttrs(snapshot): with patch("builtins.open"): top = TopologyParsers() - ca = ConnectivityAttrs(top.attrs) - assert ca.lines == snapshot + attrs = get_format_attrs(top) + ca = ConnectivityAttrs(attrs) + assert ca.table_writer.lines == snapshot diff --git a/maintainer/update_json_stubs_sitemap.py b/maintainer/update_json_stubs_sitemap.py index 921dea637..b38549d3c 100644 --- a/maintainer/update_json_stubs_sitemap.py +++ b/maintainer/update_json_stubs_sitemap.py @@ -16,11 +16,7 @@ import shutil import textwrap import xml.etree.ElementTree as ET - -try: - from urllib.request import Request, urlopen -except ImportError: - from urllib2 import Request, urlopen +from urllib.request import Request, urlopen URL = os.environ["URL"] VERSION = os.environ["VERSION"] diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 000000000..0c95025b6 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,4 @@ +[mypy] +mypy_path = doc/source/scripts/ +exclude = + conf.py diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..3790c0f32 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,11 @@ +[tool.isort] +profile = "black" + +[tool.flake8] +max-line-length = 130 +extend-ignore = ["E203", "PEA001"] +exclude = [ + "doc/source/conf.py", + "doc/source/scripts/clean_example_notebooks.py", + "maintainer/update_json_stubs_sitemap.py", +]