From 3d04290611af1df976f6002dc93ccc9013f2bda1 Mon Sep 17 00:00:00 2001 From: wpk Date: Fri, 26 Jan 2024 21:44:40 -0500 Subject: [PATCH] chore: update cruft --- .cruft.json | 2 +- .gitignore | 6 +- .pre-commit-config.yaml | 68 ++++++---- .prettierignore | 11 ++ docs/_static/js/leave_notice.js | 2 +- docs/conf.py | 7 +- noxfile.py | 38 +++--- pyproject.toml | 173 ++++++++++---------------- src/pyproject2conda/_typing_compat.py | 2 +- src/pyproject2conda/cli.py | 18 ++- src/pyproject2conda/config.py | 29 +++-- src/pyproject2conda/overrides.py | 4 +- src/pyproject2conda/requirements.py | 30 ++++- src/pyproject2conda/utils.py | 10 +- tests/test_config.py | 62 ++++++++- tests/test_parser.py | 56 +++++++++ tools/clean_kernelspec.py | 11 +- tools/common_utils.py | 2 +- tools/create_pythons.py | 19 ++- tools/dataclass_parser.py | 32 +++-- tools/noxtools.py | 91 +++++++++----- tools/projectconfig.py | 12 +- 22 files changed, 432 insertions(+), 253 deletions(-) create mode 100644 .prettierignore diff --git a/.cruft.json b/.cruft.json index a895a3c..0a42b85 100644 --- a/.cruft.json +++ b/.cruft.json @@ -1,6 +1,6 @@ { "template": "https://github.com/usnistgov/cookiecutter-nist-python.git", - "commit": "2a4d3328efe5f9abfc9407c18a8146d5cda69eb0", + "commit": "88c234d5a8d5a0c71c08bde51be21abb83bc09aa", "checkout": "develop", "context": { "cookiecutter": { diff --git a/.gitignore b/.gitignore index 2b61566..2969471 100644 --- a/.gitignore +++ b/.gitignore @@ -102,13 +102,13 @@ ENV/ # NOTE: use `/.mypy_cache/`, etc because want to flag those in other # mypy -/.mypy_cache/ +.mypy_cache/ # pytpe -/.pytype/ +.pytype/ # ruff -/.ruff_cache +.ruff_cache # PDF output README.pdf diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5fab345..e847062 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -28,9 +28,8 @@ repos: - id: sync-pre-commit-deps # * Pyproject-fmt - # NOTE: This is pretty different from prettier formatting. - repo: https://github.com/tox-dev/pyproject-fmt - rev: "1.5.3" + rev: "1.7.0" hooks: - id: pyproject-fmt args: ["--indent=4", "--keep-full-version"] @@ -38,19 +37,17 @@ repos: # * Prettier - repo: https://github.com/pre-commit/mirrors-prettier - rev: "v4.0.0-alpha.7" + rev: "v4.0.0-alpha.8" hooks: - id: prettier alias: markdownlint stages: [commit] additional_dependencies: - prettier-plugin-toml - # defer to project-fmt for pyproject.toml - exclude: pyproject.toml|^requirements/lock/.*[.]yml|^.copier-answers.ya?ml # * Markdown - repo: https://github.com/DavidAnson/markdownlint-cli2 - rev: v0.11.0 + rev: v0.12.1 hooks: - id: markdownlint-cli2 alias: markdownlint @@ -63,35 +60,60 @@ repos: hooks: - id: blacken-docs additional_dependencies: - - black==23.11.0 - # exclude: ^README.md + - black==24.1.0 - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.1.8" + rev: "v0.1.14" hooks: - id: ruff + types_or: ["python", "pyi", "jupyter"] args: ["--fix", "--show-fixes"] - id: ruff-format - - - repo: https://github.com/nbQA-dev/nbQA - rev: 1.7.1 - hooks: - - id: nbqa-ruff - additional_dependencies: [ruff==0.1.8] - # Replace with builtin if/when available - - id: nbqa - alias: nbqa-ruff-format - name: nbqa-ruff-format - additional_dependencies: [ruff==0.1.8] - args: ["ruff format --force-exclude"] + types_or: ["python", "pyi", "jupyter"] # * Spelling - repo: https://github.com/codespell-project/codespell rev: v2.2.6 hooks: - id: codespell - types_or: [python, rst, markdown, cython, c, jupyter] additional_dependencies: [tomli] - args: [-I, docs/spelling_wordlist.txt] + args: ["-I", "docs/spelling_wordlist.txt"] + exclude_types: [jupyter] + + # * Notebook formatting + - repo: https://github.com/nbQA-dev/nbQA + rev: 1.7.1 + hooks: + - id: nbqa + alias: nbqa-codespell + name: nbqa-codespell + additional_dependencies: [codespell==2.2.6, tomli] + args: + [ + "codespell", + "--ignore-words=docs/spelling_wordlist.txt", + "--nbqa-shell", + ] + - id: nbqa + alias: nbqa-codespell + name: nbqa-codespell-markdown + additional_dependencies: [codespell==2.2.6, tomli] + args: + [ + "codespell", + "--ignore-words=docs/spelling_wordlist.txt", + "--nbqa-md", + "--nbqa-shell", + ] + + # Use ruff for this instead ... + # - id: nbqa-ruff + # additional_dependencies: [ruff==0.1.14] + # # Replace with builtin if/when available + # - id: nbqa + # alias: nbqa-ruff-format + # name: nbqa-ruff-format + # additional_dependencies: [ruff==0.1.14] + # args: ["ruff format --force-exclude"] # * Commit message - repo: https://github.com/commitizen-tools/commitizen diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..9c64faf --- /dev/null +++ b/.prettierignore @@ -0,0 +1,11 @@ +# Ignore file for prettier +# Similar to .gitignore + +# Use pyproject-fmt for pyproject.toml +pyproject.toml + +# Don't fix yaml files under requirements/lock. These are created by conda-lock. +/requirements/lock/*.y*ml + +# Don't fix copier answers +.copier-answers.y*ml diff --git a/docs/_static/js/leave_notice.js b/docs/_static/js/leave_notice.js index bf45e3e..47d6447 100644 --- a/docs/_static/js/leave_notice.js +++ b/docs/_static/js/leave_notice.js @@ -1,6 +1,6 @@ $(document).ready(function () { // Mark external (non-nist.gov) A tags with class "external" - //If the adress start with https and ends with nist.gov + //If the address start with https and ends with nist.gov var re_nist = new RegExp("^https?://((^/)*.)*nist\\.gov(/|$)"); //Regex to find address that start with https var re_absolute_address = new RegExp("^((https?:)?//)"); diff --git a/docs/conf.py b/docs/conf.py index 1298d87..f6f053b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # # python_boilerplate documentation build configuration file, created by # sphinx-quickstart on Fri Jun 9 13:47:02 2017. @@ -328,14 +327,14 @@ def _get_version() -> str: # Sometimes the savefig directory doesn't exist and needs to be created # https://github.com/ipython/ipython/issues/8733 # becomes obsolete when we can pin ipython>=5.2; see ci/requirements/doc.yml -def get_ipython_savefig_dir() -> str: +def _get_ipython_savefig_dir() -> str: d = Path(__file__).parent / "_build" / "html" / "_static" if not d.is_dir(): d.mkdir(parents=True) return str(d) -ipython_savefig_dir = get_ipython_savefig_dir() +ipython_savefig_dir = _get_ipython_savefig_dir() # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. @@ -434,7 +433,7 @@ def get_ipython_savefig_dir() -> str: # based on numpy doc/source/conf.py def linkcode_resolve(domain: str, info: dict[str, Any]) -> str | None: - """Determine the URL corresponding to Python object""" + """Determine the URL corresponding to Python object.""" import inspect from operator import attrgetter diff --git a/noxfile.py b/noxfile.py index 8c4e7a1..9d6e42c 100644 --- a/noxfile.py +++ b/noxfile.py @@ -8,7 +8,9 @@ from functools import lru_cache, wraps # Should only use on python version > 3.10 -assert sys.version_info >= (3, 10) +if sys.version_info < (3, 10): + msg = "python>=3.10 required" + raise RuntimeError(msg) from dataclasses import dataclass from pathlib import Path @@ -273,6 +275,8 @@ def parse_posargs(*posargs: str) -> SessionParams: def add_opts( func: Callable[[Session, SessionParams], None], ) -> Callable[[Session], None]: + """Fill in `opts` from cli options.""" + @wraps(func) def wrapped(session: Session) -> None: opts = parse_posargs(*session.posargs) @@ -289,7 +293,6 @@ def dev( opts: SessionParams, ) -> None: """Create development environment using either conda (dev) or virtualenv (dev-venv) in location `.venv`""" - ( Installer.from_envname( session=session, @@ -341,7 +344,6 @@ def pyproject2conda( session: Session, ) -> None: """Alias to reqs""" - session.notify("requirements") @@ -356,7 +358,6 @@ def requirements( These will be placed in the directory "./requirements". """ - runner = Installer( session=session, pip_deps="pyproject2conda>=0.11.0", @@ -386,7 +387,6 @@ def conda_lock( opts: SessionParams, ) -> None: """Create lock files using conda-lock.""" - ( Installer( session=session, @@ -464,7 +464,6 @@ def pip_compile( opts: SessionParams, ) -> None: """Run pip-compile.""" - runner = Installer( session=session, pip_deps=["pip-tools"], @@ -500,7 +499,10 @@ def pip_compile( envs = envs_all for env in envs: - assert isinstance(session.python, str) + if not isinstance(session.python, str): + msg = "session.python must be a string" + raise TypeError(msg) + reqspath = infer_requirement_path(env, ext=".txt", check_exists=False) if not reqspath.is_file(): if env in envs_dev_optional: @@ -596,6 +598,7 @@ def test( @nox.session(name="test-notebook", **DEFAULT_KWS) @add_opts def test_notebook(session: nox.Session, opts: SessionParams) -> None: + """Run pytest --nbval.""" ( Installer.from_envname( session=session, @@ -636,6 +639,7 @@ def coverage( session: Session, opts: SessionParams, ) -> None: + """Run coverage.""" runner = Installer( session=session, pip_deps="coverage[toml]", @@ -670,7 +674,6 @@ def testdist( session: Session, ) -> None: """Test conda distribution.""" - opts = parse_posargs(*session.posargs) install_str = PACKAGE_NAME @@ -713,7 +716,9 @@ def docs( opts: SessionParams, ) -> None: """ - Runs make in docs directory. For example, 'nox -s docs -- +d html' + Run `make` in docs directory. + + For example, 'nox -s docs -- +d html' calls 'make -C docs html'. With 'release' option, you can set the message with 'message=...' in posargs. """ @@ -902,7 +907,9 @@ def build(session: nox.Session, opts: SessionParams) -> None: out = session.run(*args, silent=opts.build_silent) if opts.build_silent: - assert isinstance(out, str) + if not isinstance(out, str): + msg = "session.run output not a string" + raise ValueError(msg) session.log(out.strip().split("\n")[-1]) @@ -922,7 +929,6 @@ def get_package_wheel( Should be straightforward to extend this to isolated builds that depend on python version (something like have session build-3.11 ....) """ - dist_location = Path(session.cache_dir) / "dist" if reuse and getattr(get_package_wheel, "_called", False): session.log("Reuse isolated build") @@ -932,7 +938,7 @@ def get_package_wheel( # save that this was called: if reuse: - get_package_wheel._called = True # type: ignore[attr-defined] + get_package_wheel._called = True # type: ignore[attr-defined] # noqa: SLF001 paths = list(dist_location.glob("*.whl")) if len(paths) != 1: @@ -958,7 +964,6 @@ def get_package_wheel( @add_opts def publish(session: nox.Session, opts: SessionParams) -> None: """Publish the distribution""" - ( Installer(session=session, pip_deps="twine", update=opts.update) .install_all(log_session=opts.log_session) @@ -1048,6 +1053,7 @@ def conda_recipe( @nox.session(name="conda-build", **CONDA_DEFAULT_KWS) @add_opts def conda_build(session: nox.Session, opts: SessionParams) -> None: + """Run `conda mambabuild`.""" runner = Installer.from_envname( session=session, update=opts.update, @@ -1090,7 +1096,6 @@ def conda_build(session: nox.Session, opts: SessionParams) -> None: @add_opts def cog(session: nox.Session, opts: SessionParams) -> None: """Run cog.""" - Installer.from_envname( session=session, update=opts.update, @@ -1102,7 +1107,6 @@ def cog(session: nox.Session, opts: SessionParams) -> None: # * Utilities ------------------------------------------------------------------------- def _create_doc_examples_symlinks(session: nox.Session, clean: bool = True) -> None: # noqa: C901 """Create symlinks from docs/examples/*.md files to /examples/usage/...""" - import os def usage_paths(path: Path) -> Iterator[Path]: @@ -1118,7 +1122,9 @@ def get_target_path( ) -> Path: path = Path(prefix_dir) / Path(usage_path) - assert all(ext.startswith(".") for ext in exts) + if not all(ext.startswith(".") for ext in exts): + msg = "Bad extensions. Should start with '.'" + raise ValueError(msg) if path.exists(): return path diff --git a/pyproject.toml b/pyproject.toml index 3b3d5c7..24668aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -101,6 +101,11 @@ pyproject2conda = "pyproject2conda.cli:app" [tool.hatch.version] source = "vcs" +[tool.hatch.build] +exclude = [ + ".*_cache", +] + [tool.hatch.metadata.hooks.fancy-pypi-readme] content-type = "text/markdown" fragments = [ @@ -114,135 +119,88 @@ fragments = [ [tool.ruff] fix = true line-length = 88 -target-version = "py38" -exclude = [ - ".bzr", - ".direnv", - ".eggs", - ".git", - ".hg", - ".mypy_cache", - ".nox", - ".pants.d", - ".pytype", - ".ruff_cache", - ".svn", - ".tox", - ".nox", +# Handle notebooks as well +extend-include = ["*.ipynb"] +extend-exclude = [ ".venv", - "venv", - "__pypackages__", - "_build", - "buck-out", - "build", - "dist", - "node_modules", + "dist-conda", # "tests/", # "src/pyproject2conda/tests", ] unsafe-fixes = true [tool.ruff.lint] +# use nbqa for ruff linting preview = true -select = [ - "F", # - pyflakes - "E", # - pycodestyle - "W", # - pycodestyle - "I", # - isort - "UP", # - pyupgrade - "D", # - pydocstyle - "YTT", # - flake8-2020 - "B", # - flake8-bugbear - "Q", # - flake8-quotes - "PLC", # - pylint - "PLE", # - pylint - "PLR", # - pylint - "PLW", # - pylint - "PIE", # - misc lints - "TID", # - tidy imports - "TCH", # - type-checking imports - "N", # - pep8-naming - "C90", # - mccabe - "ANN", # - flake8-annotation - "BLE", # - flake8-blind-except - "A", # - flake8-builtins - "C4", # - flake8-comprehensions - "EM", # - flake8-errmsg - "FA", # - flake8-future-annotations - "ICN", # - flake8-import-conventions - "T20", # - flake8-print - "PT", # - flake8-pytest-style - "RET", # - flake8-return - "SIM", # - flake8-simplify - "ARG", # - flake8-unused-arguments - "PTH", # - flake8-use-pathlib - "TD", # - flake8-todos - "FIX", # - flake8-fixme - "PGH", # - pygrep-hooks - "FLY", # - flynt - "PERF", # - Perflint - "FURB", # - refurb - "LOG", # - flake8-loggin - "RUF", # - ruff specific - # Possibly useful? - # "G", # - flake8-logging-format - # "ERA", # - eradicate - # "PD", # - pandas-vet - # "NPY", # - numpy-specific-rules - # Overkill - # "SLF", # - flake8-self - # These conflict with formatter - # "COM", # - flake8-commas - # "ISC", # - flake8-implicit-str-concat - -] +select = ["ALL"] # Allow autofix for all enabled rules (when `--fix`) is provided. # fixable = ["A", "B", "C", "D", "E", "F", "..."] -unfixable = [] +# unfixable = [] # Exclude a variety of commonly ignored directories. ignore = [ - "E402", # - module level import not at top of file - "E501", # - line too long - let black worry about that - "E731", # - do not assign a lambda expression, use a def - "D105", # - Missing magic method docstring - "D202", # - blank line after docstring - "D205", # - blank line after summary - # this leads to errors with placing titles in module - "D102", # - Missing docstring in public method - "D103", # - Missing docstring in public function - "D400", # - First line should end with a period - "D401", # - First line of docstring should be in imperative mood: "{first_line}" - "D415", # - First line should end with a period, question mark, or exclamation point - # these are useful, but too many errors with docfiller - "D107", # - Missing docstring in __init__ - "D203", # - 1 blank line required before class docstring - "D212", # - Multi-line docstring summary should start at the first line - "D417", # - Missing argument description in the docstring for {definition}: {name} - "TID252", # - Allow relative imports - # New - "ANN101", - "ANN102", - "ANN401", - # pylint - "PLR2004", - # "PLR0913", - # "PLR0917", - "PLC0415", # - import should be at top level (leads to issues with imports in func?) + "PD", # - not using pandas? + "NPY", # - not using numpy? + "CPY", # - Don't require copyright + "ERA", # - eradicate (want to keep some stuff) + "FBT", # - bools are ok + # * Annotations + # "SLF001", # - flake8-self (private access sometimes OK) + # "ANN", # - Annotations (just use mypy/pyright) + "ANN101", # - Leads to issues with methods and self + "ANN102", # - Leads to issues with classmethods and cls + "ANN401", # - Any ok sometimes + # * pylint + # "PLR2004", # - numbers in comparison sometimes ok + # "PLR0913", # - complexity sometimes ok + # "PLR0917", # - complexity sometimes ok + # * Allow non top level imports + "PLC0415", # - import should be at top level (leads to issues with imports in func?) + "TID252", # - Allow relative imports + "E402", # - module level import not at top of file + # * Other + "E501", # - line too long - let formatter fix this + "E731", # - do not assign a lambda expression, use a def + # * Docstrings + "D105", # - Missing magic method docstring + "D205", # - blank line after summary + # * This leads to errors with placing titles in module + # "D102", # - Missing docstring in public method + # "D103", # - Missing docstring in public function + "D400", # - First line should end with a period + "D401", # - First line of docstring should be in imperative mood: "{first_line}" + "D415", # - First line should end with a period, question mark, or exclamation point + # * Not a fan of these. And lead to issues with docfiller + # "D202", # - blank line after docstring + "D107", # - Missing docstring in __init__ + "D203", # - 1 blank line required before class docstring + "D212", # - Multi-line docstring summary should start at the first line + "D417", # - Missing argument description in the docstring for {definition}: {name} (bad with docfiller) + # * These conflict with formatter + "COM", # - flake8-commas, formatter should take care of this? + "ISC", # - flake8-implicit-str-concat ] # Allow unused variables when underscore-prefixed. dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" +[tool.ruff.lint.pydocstyle] +convention = "numpy" + [tool.ruff.lint.pylint] max-args = 15 max-positional-args = 15 max-public-methods = 30 - [tool.ruff.lint.per-file-ignores] +"docs/conf.py" = ["INP001"] +"tests/*.py" = ["D", "S101"] "noxfile.py" = ["RUF009"] -"tools/dataclass_parser.py" = ["A002", "A003"] -"**/cli.py" = ["FA100", "ARG001", "T201", "PLR0913", "PLR0917"] -"tests/*.py" = ["D100", "ANN", "PT011"] +"tools/*.py" = ["S", "A", "SLF001"] +"**/*.ipynb" = ["D100", "B018", "INP001"] + +[tool.ruff.lint.extend-per-file-ignores] +"tests/*.py" = ["ANN", "PT011", "PLC2701", "PLR2004"] +"**/cli.py" = ["FA100", "ARG001", "T201", "PLR0913", "PLR0917", "D102"] +"tools/cog_utils.py" = ["D"] [tool.ruff.lint.isort] known-first-party = ["pyproject2conda"] @@ -250,9 +208,6 @@ known-first-party = ["pyproject2conda"] [tool.ruff.format] docstring-code-format = true -[tool.nbqa.addopts] -ruff = ["--fix", "--extend-ignore=D100,B018"] - # * Testing -------------------------------------------------------------------- [tool.pytest.ini_options] diff --git a/src/pyproject2conda/_typing_compat.py b/src/pyproject2conda/_typing_compat.py index 7425efb..a133aca 100644 --- a/src/pyproject2conda/_typing_compat.py +++ b/src/pyproject2conda/_typing_compat.py @@ -19,7 +19,7 @@ __all__ = [ - "TypeAlias", "Annotated", "Self", + "TypeAlias", ] diff --git a/src/pyproject2conda/cli.py b/src/pyproject2conda/cli.py index 6bce56c..0121e9b 100644 --- a/src/pyproject2conda/cli.py +++ b/src/pyproject2conda/cli.py @@ -76,7 +76,7 @@ def version_callback(value: bool) -> None: """Versioning call back.""" if value: typer.echo(f"pyproject2conda, version {__version__}") - raise typer.Exit() + raise typer.Exit @app_typer.callback() @@ -376,7 +376,9 @@ def _get_requirement_parser(filename: Union[str, Path]) -> ParseDepends: def _log_skipping( logger: logging.Logger, style: str, output: Union[str, Path, None] ) -> None: - logger.info(f"Skipping {style} {output}. Pass `-w force` to force recreate output") + logger.info( + "Skipping %s %s. Pass `-w force` to force recreate output", style, output + ) def _log_creating( @@ -430,9 +432,8 @@ def wrapped(*args: Any, **kwargs: Any) -> R: # add error logger to function call try: return func(*args, **kwargs) - except Exception as error: - # logger.exception(str(error)) - logger.error(str(error)) + except Exception: + logger.exception("found error") raise return wrapped @@ -450,8 +451,7 @@ def create_list( verbose: VERBOSE_CLI = None, ) -> None: """List available extras.""" - - logger.info(f"filename: {filename}") + logger.info("filename: %s", filename) d = _get_requirement_parser(filename) @@ -486,7 +486,6 @@ def yaml( remove_whitespace: Annotated[bool, REMOVE_WHITESPACE_OPTION] = True, ) -> None: """Create yaml file from dependencies and optional-dependencies.""" - if not update_target(output, filename, overwrite=overwrite.value): _log_skipping(logger, "yaml", output) return @@ -541,7 +540,6 @@ def requirements( remove_whitespace: Annotated[bool, REMOVE_WHITESPACE_OPTION] = True, ) -> None: """Create requirements.txt for pip dependencies.""" - if not update_target(output, filename, overwrite=overwrite.value): _log_skipping(logger, "requirements", output) return @@ -663,7 +661,6 @@ def conda_requirements( conda install --file {path_conda} pip install -r {path_pip} """ - python_include, python_version = parse_pythons( python_include=python_include, python_version=python_version, @@ -733,7 +730,6 @@ def to_json( "pip": pip dependencies. "channels": conda channels. """ - if not update_target(output, filename, overwrite=overwrite.value): _log_skipping(logger, "yaml", output) return diff --git a/src/pyproject2conda/config.py b/src/pyproject2conda/config.py index ded502b..9dbc736 100644 --- a/src/pyproject2conda/config.py +++ b/src/pyproject2conda/config.py @@ -69,13 +69,14 @@ def _get_value( default: Any = None, ) -> Any: """Get a value from thing""" - if env_name is None: value = self.get_in(key, default=None) else: # try to get from env definition - assert env_name in self.data["envs"], f"env {env_name} not in config" + if env_name not in self.data["envs"]: + msg = f"env {env_name} not in config" + raise ValueError(msg) value = self.get_in("envs", env_name, key, default=None) @@ -108,7 +109,6 @@ def python( self, env_name: str | None = None, inherit: bool = True, default: Any = list ) -> list[str]: """Python getter""" - # if callable(default): # default = default() @@ -128,7 +128,6 @@ def extras(self, env_name: str) -> list[str]: * If value is `False`, return [] * else return list of extras """ - val = self._get_value( key="extras", env_name=env_name, @@ -169,6 +168,7 @@ def base(self, env_name: str, default: bool = True) -> bool: return self._get_value(key="base", env_name=env_name, default=default) # type: ignore[no-any-return] def name(self, env_name: str) -> bool: + """Name option.""" return self._get_value(key="name", env_name=env_name) # type: ignore[no-any-return] def header(self, env_name: str) -> bool: @@ -181,7 +181,9 @@ def style(self, env_name: str | None = None, default: str = "yaml") -> str: key="style", env_name=env_name, default=default, as_list=True ) for k in out: - assert k in {"yaml", "requirements", "conda-requirements", "json"} + if k not in {"yaml", "requirements", "conda-requirements", "json"}: + msg = f"unknown style {k}" + raise ValueError(msg) return out # type: ignore[no-any-return] def python_include(self, env_name: str | None = None) -> str | None: @@ -213,6 +215,7 @@ def template_python(self, env_name: str, default: str = "py{py}-{env}") -> str: ) def deps(self, env_name: str, default: Any = None) -> list[str]: + """Conda dependencies option.""" return self._get_value( # type: ignore[no-any-return] key="deps", env_name=env_name, @@ -220,6 +223,7 @@ def deps(self, env_name: str, default: Any = None) -> list[str]: ) def reqs(self, env_name: str, default: Any = None) -> list[str]: + """Pip dependencies option.""" return self._get_value( # type: ignore[no-any-return] key="reqs", env_name=env_name, @@ -231,6 +235,7 @@ def user_config(self, env_name: str | None = None) -> str | None: # noqa: ARG00 return self._get_value(key="user_config", default=None) # type: ignore[no-any-return] def allow_empty(self, env_name: str | None = None, default: bool = False) -> bool: + """Allow empty option.""" return self._get_value( # type: ignore[no-any-return] key="allow_empty", env_name=env_name, default=default ) @@ -238,6 +243,7 @@ def allow_empty(self, env_name: str | None = None, default: bool = False) -> boo def remove_whitespace( self, env_name: str | None = None, default: bool = True ) -> bool: + """Remove whitespace option.""" return self._get_value( # type: ignore[no-any-return] key="remove_whitespace", env_name=env_name, @@ -263,10 +269,14 @@ def assign_user_config(self, user: Self) -> Self: if u is not None: d = data[key] if isinstance(d, list): - assert isinstance(u, list) + if not isinstance(u, list): + msg = f"expected list, got {type(u)}" + raise TypeError(msg) d.extend(u) # pyright: ignore[reportUnknownMemberType] elif isinstance(d, dict): - assert isinstance(u, dict) + if not isinstance(u, dict): + msg = f"expected dict, got {type(u)}" + raise TypeError(msg) d.update(**u) # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType] return type(self)(data) @@ -362,7 +372,6 @@ def iter_envs( self, envs: Sequence[str] | None = None, **defaults: Any ) -> Iterator[tuple[str, dict[str, Any]]]: """Iterate over configs""" - # filter defaults. Only include values of not None: defaults = {k: v for k, v in defaults.items() if v is not None} @@ -375,9 +384,9 @@ def iter_envs( yield from self._iter_yaml(env, **defaults) elif style == "requirements": yield from self._iter_reqs(env, **defaults) - else: + else: # pragma: no cover msg = f"unknown style {style}" - raise ValueError(msg) # pragma: no cover + raise ValueError(msg) @classmethod def from_toml_dict( diff --git a/src/pyproject2conda/overrides.py b/src/pyproject2conda/overrides.py index 5c21c92..6a5535e 100644 --- a/src/pyproject2conda/overrides.py +++ b/src/pyproject2conda/overrides.py @@ -19,6 +19,7 @@ # * Comment parsing -------------------------------------------------------------------- @lru_cache def p2c_argparser() -> argparse.ArgumentParser: + """Parser for p2c comment options.""" parser = argparse.ArgumentParser( description="Parser searches for comments '# p2c: [OPTIONS] CONDA-PACKAGES'" ) @@ -58,7 +59,6 @@ def _match_p2c_comment(comment: OptStr) -> OptStr: def _parse_p2c(match: OptStr) -> OverrideDict | None: """Parse match from _match_p2c_comment""" - if match: return cast(OverrideDict, vars(p2c_argparser().parse_args(shlex.split(match)))) return None @@ -111,6 +111,7 @@ def __repr__(self) -> str: # pragma: no cover def from_comment( cls, comment: str | None, default: OverrideDict | None = None ) -> Self | None: + """Create from comment.""" parsed = _parse_p2c_comment(comment) kws: OverrideDict @@ -131,6 +132,7 @@ def requirement_comment_to_override_pairs( requirement_comment_pairs: list[RequirementCommentPair], override_table: dict[str, OverrideDict], ) -> list[RequirementOverridePair]: + """Create from override pairs.""" out: list[RequirementOverridePair] = [] for requirement, comment in requirement_comment_pairs: if requirement is not None: diff --git a/src/pyproject2conda/requirements.py b/src/pyproject2conda/requirements.py index d5558ca..115887b 100644 --- a/src/pyproject2conda/requirements.py +++ b/src/pyproject2conda/requirements.py @@ -151,7 +151,7 @@ def _iter_value_comment_pairs( array: tomlkit.items.Array, ) -> Generator[tuple[OptStr, OptStr], None, None]: """Extract value and comments from array""" - for v in array._value: # pyright: ignore[reportPrivateUsage] + for v in array._value: # pyright: ignore[reportPrivateUsage] # noqa: SLF001 if v.value is not None and not isinstance(v.value, tomlkit.items.Null): value = str(v.value) else: @@ -334,12 +334,14 @@ def __init__(self, data: tomlkit.toml_document.TOMLDocument) -> None: def get_in( self, *keys: str, default: Any = None, factory: Callable[[], Any] | None = None ) -> Any: + """Generic getter.""" return get_in( keys=keys, nested_dict=self.data, default=default, factory=factory ) @cached_property def package_name(self) -> str: + """Clean name of package.""" if (out := self.get_in("project", "name")) is None: msg = "Must specify `project.name`" raise ValueError(msg) @@ -377,11 +379,13 @@ def optional_dependencies(self) -> tomlkit.items.Table: @cached_property def override_table(self) -> dict[str, OverrideDict]: + """tool.pyproject2conda.dependencies""" out = self.get_in("tool", "pyproject2conda", "dependencies", default=MISSING) return cast("dict[str, OverrideDict]", {} if out is MISSING else out.unwrap()) @cached_property def channels(self) -> list[str]: + """tool.pyproject2conda.channels""" channels_doc = self.get_in("tool", "pyproject2conda", "channels") if channels_doc: channels = channels_doc.unwrap() @@ -393,6 +397,7 @@ def channels(self) -> list[str]: @property def extras(self) -> list[str]: + """build-system.requires""" return [*self.optional_dependencies.keys(), "build-system.requires"] @cached_property @@ -503,6 +508,7 @@ def pip_requirements( remove_whitespace: bool = True, sort: bool = True, ) -> list[str]: + """Pip dependencies.""" self._check_extras(extras) out: list[str] = [ @@ -521,7 +527,7 @@ def pip_requirements( out, remove_whitespace=remove_whitespace, unique=unique, sort=sort ) - def conda_and_pip_requirements( # noqa: C901 + def conda_and_pip_requirements( # noqa: C901, PLR0912 self, extras: str | Iterable[str] | None = None, include_base: bool = True, @@ -533,6 +539,8 @@ def conda_and_pip_requirements( # noqa: C901 python_version: str | None = None, python_include: str | None = None, ) -> tuple[list[str], list[str]]: + """Conda and pip requirements.""" + def _init_deps(deps: str | Iterable[str] | None) -> list[str]: if deps is None: return [] @@ -559,10 +567,15 @@ def _init_deps(deps: str | Iterable[str] | None) -> list[str]: ): if override is not None: if override.pip: - assert requirement is not None + if requirement is None: + msg = "requirement is None" + raise TypeError(msg) pip_deps.append(str(requirement)) + elif not override.skip: - assert requirement is not None + if requirement is None: + msg = "requirement is None" + raise TypeError(msg) r = _clean_conda_requirement( requirement, @@ -625,6 +638,7 @@ def to_conda_yaml( unique: bool = True, allow_empty: bool = False, ) -> str: + """Create yaml string.""" self._check_extras(extras) conda_deps, pip_deps = self.conda_and_pip_requirements( @@ -666,6 +680,7 @@ def to_requirements( allow_empty: bool = False, remove_whitespace: bool = True, ) -> str: + """Create requirements string.""" pip_deps = self.pip_requirements( extras=extras, include_base=include_base, @@ -699,6 +714,7 @@ def to_conda_requirements( pip_deps: str | Iterable[str] | None = None, remove_whitespace: bool = True, ) -> tuple[str, str]: + """Create conda and pip requirements files.""" conda_deps, pip_deps = self.conda_and_pip_requirements( extras=extras, include_base=include_base, @@ -719,7 +735,9 @@ def to_conda_requirements( channels = self.channels if conda_deps and channels and prepend_channel: - assert len(channels) == 1 + if len(channels) != 1: + msg = "Can only pass single channel to prepend." + raise ValueError(msg) channel = channels[0] # add in channel if none exists conda_deps = [ @@ -742,11 +760,13 @@ def from_string( cls, toml_string: str, ) -> Self: + """Create object from string.""" data = tomlkit.parse(toml_string) return cls(data=data) @classmethod def from_path(cls, path: str | Path) -> Self: + """Create object from path.""" with Path(path).open("rb") as f: data = tomlkit.load(f) return cls(data=data) diff --git a/src/pyproject2conda/utils.py b/src/pyproject2conda/utils.py index e288e52..3da2b8d 100644 --- a/src/pyproject2conda/utils.py +++ b/src/pyproject2conda/utils.py @@ -72,6 +72,7 @@ def parse_pythons( python_version: str | None, python: str | None, ) -> tuple[str | None, str | None]: + """Create python_include/python_version.""" if python: return f"python={python}", python return python_include, python_version @@ -83,7 +84,6 @@ def update_target( overwrite: str = "check", ) -> bool: """Check if target is older than deps:""" - if target is None: # No output file. always run. return True @@ -105,9 +105,9 @@ def update_target( target_time = target.stat().st_mtime update = any(target_time < dep.stat().st_mtime for dep in deps_filtered) - else: + else: # pragma: no cover msg = f"unknown option overwrite={overwrite}" - raise ValueError(msg) # pragma: no cover + raise ValueError(msg) return update @@ -129,7 +129,6 @@ def filename_from_template( env : name of environment """ - if template is None: return None @@ -158,10 +157,12 @@ def filename_from_template( def remove_whitespace(s: str) -> str: + """Cleanup whitespace from string.""" return re.sub(_WHITE_SPACE_REGEX, "", s) def remove_whitespace_list(s: Iterable[str]) -> list[str]: + """Cleanup whitespace from list of strings.""" return [remove_whitespace(x) for x in s] @@ -178,6 +179,7 @@ def unique_list(values: Iterable[T]) -> list[T]: def list_to_str(values: Iterable[str] | None, eol: bool = True) -> str: + """Join list of strings with newlines to single string.""" if values: output = "\n".join(values) if eol: diff --git a/tests/test_config.py b/tests/test_config.py index b8cbd03..a5abe36 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -278,6 +278,37 @@ def test_config_only_default() -> None: assert list(c.iter_envs()) == expected +def test_config_errors() -> None: + s = """ + [tool.pyproject2conda] + python = ["3.8"] + + [tool.pyproject2conda.envs.test] + extras = true + """ + + # raise error for bad env + c = Config.from_string(s) + with pytest.raises(ValueError): + c.channels(env_name="hello") + + s1 = """ + [tool.pyproject2conda] + python = ["3.8"] + + [tool.pyproject2conda.envs.test] + style = "thing" + """ + + # raise error for bad env + c = Config.from_string(s1) + with pytest.raises(ValueError): + c.style(env_name="test") + + with pytest.raises(ValueError): + list(c.iter_envs()) + + def test_config_overrides() -> None: # test overrides env s = """ @@ -460,12 +491,39 @@ def test_config_user_config() -> None: assert list(c.iter_envs()) == expected + # bad user + s_user2 = """ + [[tool.pyproject2conda.envs]] + extras = ["a", "b"] + python = "3.9" + + [[tool.pyproject2conda.overrides]] + envs = ["test"] + base = false + """ + + with pytest.raises(TypeError): + c = Config.from_string(s, s_user2) + + s_user2 = """ + [tool.pyproject2conda.envs] + extras = ["a", "b"] + python = "3.9" + + [tool.pyproject2conda.overrides] + envs = ["test"] + base = false + """ + + with pytest.raises(TypeError): + c = Config.from_string(s, s_user2) + # blank config, only user - s = """ + s2 = """ [tool.pyproject2conda] """ - c = Config.from_string(s, s_user) + c = Config.from_string(s2, s_user) assert c.data == { "envs": {"user": {"extras": ["a", "b"], "python": "3.9"}}, diff --git a/tests/test_parser.py b/tests/test_parser.py index 9dcbac3..c5f3166 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -301,6 +301,62 @@ def test_pip_requirements() -> None: assert d.to_requirements(pip_deps="hello") == expected +def test_bad_comment_pip() -> None: + toml = dedent( + """\ + [project] + requires-python = ">=3.8,<3.11" + dependencies = [ + # p2c: -p # a comment + "bthing", # p2c: -s bthing-conda + "cthing; python_version<'3.10'", # p2c: -c conda-forge + ] + """ + ) + + d = requirements.ParseDepends.from_string(toml) + + with pytest.raises(TypeError): + assert d.to_conda_yaml() + + +def test_bad_comment_conda() -> None: + toml = dedent( + """\ + [project] + requires-python = ">=3.8,<3.11" + dependencies = [ + "athing", + # p2c: -c hello + ] + """ + ) + + d = requirements.ParseDepends.from_string(toml) + + with pytest.raises(TypeError): + assert d.to_conda_yaml() + + +def test_to_conda_requirements_error() -> None: + toml = dedent( + """\ + [project] + requires-python = ">=3.8,<3.11" + dependencies = [ + "athing", # p2c: -p # a comment + "bthing", # p2c: -s bthing-conda + "cthing; python_version<'3.10'", # p2c: -c conda-forge + ] + """ + ) + + d = requirements.ParseDepends.from_string(toml) + + with pytest.raises(ValueError): + d.to_conda_requirements(channels=["hello", "there"], prepend_channel=True) + + def test_package_name() -> None: toml = dedent( """\ diff --git a/tools/clean_kernelspec.py b/tools/clean_kernelspec.py index 21f3910..43e6ecb 100644 --- a/tools/clean_kernelspec.py +++ b/tools/clean_kernelspec.py @@ -32,14 +32,14 @@ def get_kernelspec_data() -> None: p = Path(data["spec"]["argv"][0]) if not p.exists(): - logger.debug(f"{name} does not exist.") + logger.debug("%s does not exist.", name) to_remove.append(name) else: - logger.debug(f"{name} exists") + logger.debug("%s exists.", name) if to_remove: - logger.info(f"removing kernels {to_remove}") + logger.info("removing kernels %s", to_remove) check_output(["jupyter", "kernelspec", "remove", "-f", *to_remove]) else: logger.info("nothing to do") @@ -48,7 +48,6 @@ def get_kernelspec_data() -> None: if __name__ == "__main__": try: get_kernelspec_data() - except CalledProcessError as e: - logger.error(e) - logger.error("Most likely you didn't run from notebook server environment") + except CalledProcessError: + logger.exception("Most likely you didn't run from notebook server environment") raise diff --git a/tools/common_utils.py b/tools/common_utils.py index b3efc82..f4da074 100644 --- a/tools/common_utils.py +++ b/tools/common_utils.py @@ -13,7 +13,7 @@ def get_conda_environment_map(simplify: bool = True) -> dict[str, str]: for line in result.decode().split("\n"): if not line.startswith("#"): x = line.replace("*", "").split() - if len(x) == 2: + if len(x) == 2: # noqa: PLR2004 env_map[x[0]] = x[1] if simplify: home = str(Path.home()) diff --git a/tools/create_pythons.py b/tools/create_pythons.py index 7346123..93e3e61 100644 --- a/tools/create_pythons.py +++ b/tools/create_pythons.py @@ -4,7 +4,9 @@ import sys from functools import lru_cache -assert sys.version_info >= (3, 9) +if sys.version_info < (3, 9): + msg = "Requires python >= 3.9" + raise RuntimeError(msg) import logging @@ -14,6 +16,7 @@ @lru_cache def conda_cmd() -> str: + """Get conda/mamba command.""" import shutil if shutil.which("mamba"): @@ -31,6 +34,7 @@ def create_env_from_spec( spec: str | list[str], flags: str | list[str] | None = None, ) -> None: + """Create environment from spec.""" import shlex import subprocess @@ -41,7 +45,7 @@ def create_env_from_spec( flags = " ".join(flags) cmd = f"{conda_cmd()} create -n {env_name} {flags} {spec} " - logging.info(f"running {cmd}") + logging.info("running %s", cmd) out = subprocess.check_call(shlex.split(cmd)) if out != 0: @@ -55,6 +59,7 @@ def create_environments( flags: str | list[str] | None = None, env_map: dict[str, str] | None = None, ) -> None: + """Create environment.""" if versions is None: versions = ["3.8", "3.9", "3.10", "3.11"] @@ -67,10 +72,11 @@ def create_environments( if env_map and env_name in env_map: # environment exists logging.info( - f"Skipping environment {env_name}. Pass `--no-skip` to force recreation.", + "Skipping environment %s. Pass `--no-skip` to force recreation.", + env_name, ) else: - logging.info(f"Creating environment {env_name}.") + logging.info("Creating environment %s.", env_name) spec = f"python={version}" create_env_from_spec( @@ -81,6 +87,7 @@ def create_environments( def main() -> None: + """Main runner.""" import argparse p = argparse.ArgumentParser( @@ -132,7 +139,7 @@ def main() -> None: if args.verbose: logger.setLevel(logging.INFO) - logging.info(f"{args=}") + logging.info(f"{args=}") # noqa: G004 flags: list[str] = [] if args.yes: @@ -164,6 +171,6 @@ def main() -> None: from pathlib import Path here = Path(__file__).absolute() - __package__ = here.parent.name # noqa: A001 + __package__ = here.parent.name main() diff --git a/tools/dataclass_parser.py b/tools/dataclass_parser.py index a8fab61..c303969 100644 --- a/tools/dataclass_parser.py +++ b/tools/dataclass_parser.py @@ -40,7 +40,9 @@ class Example(DataclassParser): replace, ) -assert sys.version_info >= (3, 10) +if sys.version_info < (3, 10): + msg = "Require python >= 3.10" + raise RuntimeError(msg) from typing import ( TYPE_CHECKING, @@ -100,6 +102,7 @@ def __post_init__(self) -> None: raise ValueError(msg) def asdict(self) -> dict[str, Any]: + """Convert to dictionary.""" return { k: v for k, v in @@ -120,6 +123,7 @@ def add_argument_to_parser( parser: ArgumentParser, prefix_char: str = "-", ) -> None: + """Add argument to parser.""" kwargs = self.asdict() flags = kwargs.pop("flags") @@ -155,8 +159,9 @@ def factory( metavar: str = UNDEFINED, nargs: str | int | None = UNDEFINED, required: bool = UNDEFINED, - type: int | float | Callable[[Any], Any] = UNDEFINED, + type: Callable[[Any], Any] = UNDEFINED, ) -> Self: + """Factory method.""" return cls( flags=flags or UNDEFINED, action=action, @@ -186,9 +191,10 @@ def add_option( metavar: str = UNDEFINED, nargs: str | int | None = UNDEFINED, required: bool = UNDEFINED, - type: int | float | Callable[[Any], Any] = UNDEFINED, + type: Callable[[Any], Any] = UNDEFINED, **field_kws: Any, # noqa: ARG001 ) -> Any: + """Add option.""" return field( metadata={ "option": Option.factory( @@ -214,6 +220,7 @@ class DataclassParser: @classmethod def parser(cls, prefix_char: str = "-", **kwargs: Any) -> ArgumentParser: + """Get parser.""" parser = ArgumentParser(prefix_chars=prefix_char, **kwargs) for opt in get_dataclass_options(cls).values(): @@ -229,6 +236,7 @@ def from_posargs( parser: ArgumentParser | None = None, known: bool = False, ) -> Self: + """Create object from posargs.""" if parser is None: parser = cls.parser(prefix_char=prefix_char) @@ -246,6 +254,7 @@ def from_posargs( def get_dataclass_options(cls: Any) -> dict[str, Option]: + """Get dictionary of options.""" return { name: _create_option(name=name, opt=opt, annotation=annotation) for name, (annotation, opt) in _get_dataclass_annotations_and_options( @@ -285,21 +294,10 @@ def _create_option( opt: Option, annotation: Any, ) -> Option: - # Can also pass via annotations - # if this is the case, explicitly options from add_option - # will take precedence. - # if get_origin(annotation) is Annotated: - # opt_type, opt_anno, *_ = get_args(annotation) - - # if isinstance(opt_anno, Option): - # opt = Option(**{**opt_anno.asdict(), **opt.asdict()}) - - # else: - # opt_type = annotation - depth, underlying_type = _get_underlying_type(annotation) + max_depth = 2 - if depth <= 2 and get_origin(underlying_type) is Literal: + if depth <= max_depth and get_origin(underlying_type) is Literal: choices = get_args(underlying_type) if opt.choices is UNDEFINED: opt = replace(opt, choices=choices) @@ -381,7 +379,7 @@ def _get_underlying_type( def _get_underlying_if_optional(t: Any, pass_through: bool = False) -> Any: if _is_union_type(t): args = get_args(t) - if len(args) == 2 and _NoneType in args: + if len(args) == 2 and _NoneType in args: # noqa: PLR2004 for arg in args: if arg != _NoneType: return arg diff --git a/tools/noxtools.py b/tools/noxtools.py index 45634c8..7121511 100644 --- a/tools/noxtools.py +++ b/tools/noxtools.py @@ -17,7 +17,6 @@ def override_sessionrunner_create_venv(self: SessionRunner) -> None: """Override SessionRunner._create_venv""" - if callable(self.func.venv_backend): # if passed a callable backend, always use just that logger.info("Using custom callable venv_backend") @@ -60,13 +59,17 @@ def factory_conda_backend( backend: Literal["conda", "mamba", "micromamba"] = "conda", location: str | None = None, ) -> Callable[..., CondaEnv]: + """Factory method for conda backend.""" + def passthrough_venv_backend( runner: SessionRunner, ) -> CondaEnv: # override allowed_globals CondaEnv.allowed_globals = ("conda", "mamba", "micromamba", "conda-lock") # type: ignore[assignment] - assert isinstance(runner.func.python, str) + if not isinstance(runner.func.python, str): + msg = "Python version is not a string" + raise TypeError(msg) return CondaEnv( location=location or runner.envdir, interpreter=runner.func.python, @@ -83,6 +86,8 @@ def factory_virtualenv_backend( backend: Literal["virtualenv", "venv"] = "virtualenv", location: str | None = None, ) -> Callable[..., CondaEnv | VirtualEnv]: + """Factory virtualenv backend.""" + def passthrough_venv_backend( runner: SessionRunner, ) -> VirtualEnv: @@ -102,6 +107,10 @@ def passthrough_venv_backend( # * Top level installation functions --------------------------------------------------- def py_prefix(python_version: Any) -> str: + """Get python prefix. + + `python="3.8` -> "py38" + """ if isinstance(python_version, str): return "py" + python_version.replace(".", "") msg = f"passed non-string value {python_version}" @@ -136,6 +145,7 @@ def infer_requirement_path_with_fallback( check_exists: bool = True, lock_fallback: bool = False, ) -> tuple[bool, Path]: + """Get the requirements file from options with fallback.""" if lock_fallback: try: path = infer_requirement_path( @@ -175,7 +185,6 @@ def infer_requirement_path( check_exists: bool = True, ) -> Path: """Get filename for a conda yaml or pip requirements file.""" - if name is None: msg = "must supply name" raise ValueError(msg) @@ -235,6 +244,7 @@ def _infer_requirement_paths( def is_conda_session(session: Session) -> bool: + """Whether session is a conda session.""" from nox.virtualenv import CondaEnv return isinstance(session.virtualenv, CondaEnv) @@ -370,6 +380,7 @@ def config(self) -> dict[str, Any]: return out def save_config(self) -> Self: + """Save config as json file to something like session/tmp/env.json""" import json # in case config path got clobbered @@ -382,6 +393,7 @@ def save_config(self) -> Self: @cached_property def previous_config(self) -> dict[str, Any]: + """Previous config.""" if not self.config_path.exists(): return {} @@ -402,11 +414,13 @@ def tmp_path(self) -> Path: return Path(self.session.virtualenv.location) / "tmp" def create_tmp_path(self) -> Path: + """Create `self.tmp_path`""" tmp = self.tmp_path self.tmp_path.mkdir(parents=True, exist_ok=True) return tmp def log_session(self) -> Self: + """Save log of session to `self.tmp_path / "env_info.txt"`.""" logfile = Path(self.create_tmp_path()) / "env_info.txt" self.session.log(f"writing environment log to {logfile}") @@ -423,9 +437,11 @@ def log_session(self) -> Self: # Interface @property def python_version(self) -> str: + """Python version for session.""" return cast(str, self.session.python) def is_conda_session(self) -> bool: + """Whether session is conda session.""" return is_conda_session(self.session) @property @@ -437,15 +453,20 @@ def env(self) -> dict[str, str]: @property def conda_cmd(self) -> str: + """Command for conda session (conda/mamba).""" venv = self.session.virtualenv - assert isinstance(venv, CondaEnv) + if not isinstance(venv, CondaEnv): + msg = "venv is not a CondaEnv" + raise TypeError(msg) return venv.conda_cmd def is_micromamba(self) -> bool: + """Whether conda session uses micromamba.""" return self.is_conda_session() and self.conda_cmd == "micromamba" @cached_property def python_full_path(self) -> str: + """Full path to session python executable.""" path = self.session.run_always( "python", "-c", @@ -454,7 +475,7 @@ def python_full_path(self) -> str: ) if not isinstance(path, str): msg = "accessing python_full_path with value None" - raise ValueError(msg) + raise TypeError(msg) return path.strip() @property @@ -463,6 +484,7 @@ def _session_runner(self) -> SessionRunner: @cached_property def skip_install(self) -> bool: + """Whether to skip install.""" return self._session_runner.global_config.no_install and getattr( self._session_runner.venv, "_reused", @@ -479,7 +501,6 @@ def package_changed(self) -> bool: @cached_property def changed(self) -> bool: """Check for changes (excluding package)""" - a, b = ( {k: v for k, v in config.items() if k != "package"} for config in (self.config, self.previous_config) @@ -499,6 +520,7 @@ def run_commands( external: bool = True, **kwargs: Any, ) -> Self: + """Run commands in session.""" if commands: kwargs.update(external=external) for opt in combine_list_list_str(commands): @@ -512,6 +534,7 @@ def set_ipykernel_display_name( user: bool = True, update: bool = False, ) -> Self: + """Set ipykernel display name.""" if self.changed or update or self.update: if display_name is None: display_name = f"Python [venv: {name}]" @@ -536,8 +559,11 @@ def install_all( log_session: bool = False, save_config: bool = True, ) -> Self: + """Install package/dependencies.""" if self.create_venv: - assert self.is_conda_session() + if not self.is_conda_session(): + msg = "Only CondaEnv should be used with create_venv" + raise TypeError(msg) self.create_conda_env() out = ( @@ -578,18 +604,6 @@ def pip_install_package( return self - @cached_property - def session_python_path(self) -> str: - return cast( - str, - self.session.run_always( - "python", - "-c", - "import sys; print(sys.executable)", - silent=True, - ), - ) - def pip_install_deps( self, *args: Any, @@ -597,6 +611,7 @@ def pip_install_deps( opts: str | Iterable[str] | None = None, **kwargs: Any, ) -> Self: + """Install pip dependencies with pip or pip-sync.""" if self.skip_install: pass @@ -632,8 +647,11 @@ def pip_install_deps( return self def create_conda_env(self, update: bool = False) -> Self: + """Create conda environment.""" venv = self.session.virtualenv - assert isinstance(venv, CondaEnv) + if not isinstance(venv, CondaEnv): + msg = "Session must be a conda session." + raise TypeError(msg) if venv._clean_location(): # pyright: ignore[reportPrivateUsage] # Also clean out session tmp directory @@ -698,6 +716,7 @@ def conda_install_deps( prune: bool = False, **kwargs: Any, ) -> Self: + """Install conda dependencies (apart from environment.yaml).""" if (not self.conda_deps) or self.skip_install: pass @@ -735,8 +754,11 @@ def from_envname_pip( requirements: PathLike | Iterable[PathLike] | None = None, **kwargs: Any, ) -> Self: + """Create object for virtualenv from envname.""" if lock: - assert isinstance(session.python, str) + if not isinstance(session.python, str): + msg = "session.python must be a string" + raise TypeError(msg) requirements = _verify_paths(requirements) + _infer_requirement_paths( envname, ext=".txt", @@ -773,6 +795,8 @@ def from_envname_conda( **kwargs: Any, ) -> Self: """ + Create object for conda environment from envname. + Parameters ---------- envname : @@ -780,9 +804,10 @@ def from_envname_conda( envname = "dev" will convert to `requirements/py{py}-dev.yaml` for `filename` """ - if envname is not None and conda_yaml is None: - assert isinstance(session.python, str) + if not isinstance(session.python, str): + msg = "session.python must be a string" + raise TypeError(msg) lock, conda_yaml = infer_requirement_path_with_fallback( envname, ext=".yaml", @@ -818,11 +843,14 @@ def from_envname( lock_fallback: bool | None = None, **kwargs: Any, ) -> Self: + """Create object from envname.""" if lock_fallback is None: lock_fallback = is_conda_session(session) if is_conda_session(session): - assert isinstance(envname, str) or envname is None + if not isinstance(envname, str) and envname is not None: + msg = "envname must be a string or None" + raise TypeError(msg) return cls.from_envname_conda( session=session, envname=envname, @@ -865,6 +893,7 @@ def _remove_whitespace_list(s: str | Iterable[str]) -> list[str]: def combine_list_str(opts: str | Iterable[str]) -> list[str]: + """Cleanup str/list[str] to list[str]""" if not opts: return [] @@ -874,6 +903,7 @@ def combine_list_str(opts: str | Iterable[str]) -> list[str]: def combine_list_list_str(opts: Iterable[str | Iterable[str]]) -> Iterable[list[str]]: + """Cleanup Iterable[str/list[str]] to Iterable[list[str]].""" return (combine_list_str(opt) for opt in opts) @@ -913,7 +943,6 @@ def prepend_flag(flag: str, *args: str | Iterable[str]) -> list[str]: >>> prepent_flag("-k", "a", "b") ["-k", "a", "-k", "b"] """ - args_: list[str] = [] for x in args: if isinstance(x, str): @@ -951,6 +980,7 @@ def _get_zipfile_hash(path: str | Path) -> str: def get_package_hash(package: str) -> list[str]: + """Get hash for wheel of package.""" import re out: list[str] = [] @@ -985,7 +1015,6 @@ def check_for_change_manager( If exit normally, write hashes to hash_path file """ - try: changed, hashes, hash_path = check_hash_path_for_change( *deps, @@ -995,8 +1024,8 @@ def check_for_change_manager( yield changed - except Exception as e: - raise e + except Exception: # noqa: TRY302 + raise else: if force_write or changed: @@ -1041,7 +1070,8 @@ def check_hash_path_for_change( msg = "Must specify target_path or hash_path" if target_path is None: - assert hash_path is not None, msg + if hash_path is None: + raise ValueError(msg) target_path = hash_path = Path(hash_path) else: target_path = Path(target_path) @@ -1076,6 +1106,7 @@ def check_hash_path_for_change( def write_hashes(hash_path: str | Path, hashes: dict[str, Any]) -> None: + """Write hashes to json file.""" import json with Path(hash_path).open("w") as f: @@ -1105,7 +1136,6 @@ def session_run_commands( **kws: Any, ) -> None: """Run commands command.""" - if commands: kws.update(external=external) for opt in combine_list_list_str(commands): @@ -1128,7 +1158,6 @@ def load_nox_config(path: str | Path = "./config/userconfig.toml") -> dict[str, [nox.extras] dev = ["dev", "nox"] """ - from .projectconfig import ProjectConfig return ProjectConfig.from_path_and_environ(path).to_nox_config() diff --git a/tools/projectconfig.py b/tools/projectconfig.py index 73959ea..73591ea 100644 --- a/tools/projectconfig.py +++ b/tools/projectconfig.py @@ -57,6 +57,7 @@ def new_like( env_extras: Mapping[str, Mapping[str, Any]] | None = None, copy: bool = True, ) -> Self: + """Create new object like current object.""" return type(self)( python_paths=python_paths or self.python_paths, env_extras=env_extras or self.env_extras, @@ -88,6 +89,7 @@ def _path_to_params( @classmethod def from_path(cls, path: str | Path = "./config/userconfig.toml") -> Self: + """Create object from path.""" path = Path(path) if path.exists(): @@ -101,6 +103,8 @@ def from_path_and_environ( cls, path: str | Path = "./config/userconfig.toml", ) -> Self: + """Create object from path and environment variable.""" + def _get_python_paths_from_environ() -> list[str]: if python_paths_environ := os.environ.get("NOX_PYTHON_PATH"): return python_paths_environ.split(":") @@ -159,6 +163,7 @@ def _params_to_string( return header + s def to_path(self, path: str | Path | None = None) -> str: + """Create output file.""" s = self._params_to_string( python_paths=self.python_paths, env_extras=self.env_extras, @@ -173,6 +178,7 @@ def __repr__(self) -> str: return f"" def expand_python_paths(self) -> list[str]: + """Expand wildcards in path""" from glob import glob paths: list[str] = [] @@ -185,6 +191,7 @@ def add_paths_to_environ( paths: list[str] | None, prepend: bool = True, ) -> None: + """Add path(s) to environment variable `PATH`""" if paths is None: paths = self.expand_python_paths() paths_str = ":".join(map(str, paths)) @@ -196,6 +203,7 @@ def to_nox_config( add_paths_to_environ: bool = True, prepend: bool = True, ) -> dict[str, Any]: + """Create nox configuration.""" config: dict[str, Any] = {} if self.python_paths: @@ -213,6 +221,7 @@ def to_nox_config( def glob_envs_to_paths(globs: list[str]) -> list[str]: + """Convert globbed environments to paths.""" import fnmatch from .common_utils import get_conda_environment_map @@ -228,6 +237,7 @@ def glob_envs_to_paths(globs: list[str]) -> list[str]: def main() -> None: + """Main runner.""" import argparse p = argparse.ArgumentParser(description="Create the file config/userconfig.toml") @@ -291,6 +301,6 @@ def main() -> None: # or # $ python tools/create_python.py here = Path(__file__).absolute() - __package__ = here.parent.name # noqa: A001 + __package__ = here.parent.name main()