diff --git a/config/coverage.ini b/config/coverage.ini index 7db748bfd..728a0ebb5 100644 --- a/config/coverage.ini +++ b/config/coverage.ini @@ -2,7 +2,8 @@ branch = true parallel = true source = - src/griffe + packages/griffelib/src/griffe + packages/griffecli/src/griffecli tests/ [coverage:paths] diff --git a/config/ruff.toml b/config/ruff.toml index 0a52cb729..9cb744122 100644 --- a/config/ruff.toml +++ b/config/ruff.toml @@ -38,24 +38,24 @@ ignore = [ logger-objects = ["griffe.logger"] [lint.per-file-ignores] -"src/griffe/__main__.py" = [ +"packages/griffecli/src/griffecli/__main__.py" = [ "D100", # Missing module docstring ] -"src/griffe/_internal/cli.py" = [ +"packages/griffecli/src/griffecli/_internal/cli.py" = [ "T201", # Print statement ] -"src/griffe/_internal/git.py" = [ +"packages/griffelib/src/griffe/_internal/git.py" = [ "S603", # `subprocess` call: check for execution of untrusted input "S607", # Starting a process with a partial executable path ] -"src/griffe/_internal/agents/nodes/*.py" = [ +"packages/griffelib/src/griffe/_internal/agents/nodes/*.py" = [ "ARG001", # Unused function argument "N812", # Lowercase `keyword` imported as non-lowercase `NodeKeyword` ] -"src/griffe/_internal/debug.py" = [ +"packages/griffelib/src/griffe/_internal/debug.py" = [ "T201", # Print statement ] -"src/griffe/_internal/**.py" = [ +"packages/griffelib/src/griffe/_internal/**.py" = [ "D100", # Missing docstring in public module ] "scripts/*.py" = [ @@ -84,7 +84,7 @@ docstring-quotes = "double" ban-relative-imports = "all" [lint.isort] -known-first-party = ["griffe"] +known-first-party = ["griffe", "griffecli"] [lint.pydocstyle] convention = "google" diff --git a/docs/guide/contributors/architecture.md b/docs/guide/contributors/architecture.md index edd5a94ec..758470687 100644 --- a/docs/guide/contributors/architecture.md +++ b/docs/guide/contributors/architecture.md @@ -23,7 +23,7 @@ descriptions = { "site": "Documentation site, built with `make run mkdocs build` (git-ignored).", "src": "The source of our Python package(s). See [Sources](#sources) and [Program structure](#program-structure).", "src/griffe": "Our public API, exposed to users. See [Program structure](#program-structure).", - "src/griffe/_internal": "Our internal API, hidden from users. See [Program structure](#program-structure).", + "packages/griffelib/src/griffe/_internal": "Our internal API, hidden from users. See [Program structure](#program-structure).", "tests": "Our test suite. See [Tests](#tests).", ".copier-answers.yml": "The answers file generated by [Copier](https://copier.readthedocs.io/en/stable/). See [Boilerplate](#boilerplate).", "devdeps.txt": "Our development dependencies specification. See [`make setup`][command-setup] command.", @@ -98,19 +98,31 @@ The tools used in tasks have their configuration files stored in the `config` fo ## Sources -Sources are located in the `src` folder, following the [src-layout](https://packaging.python.org/en/latest/discussions/src-layout-vs-flat-layout/). We use [PDM-Backend](https://backend.pdm-project.org/) to build source and wheel distributions, and configure it in `pyproject.toml` to search for packages in the `src` folder. +Sources are located in the `packages/` subfolders, following the [src-layout](https://packaging.python.org/en/latest/discussions/src-layout-vs-flat-layout/). +We use [Hatch](https://hatch.pypa.io/latest/) to build source and wheel distributions, and configure it in `pyproject.toml`. ## Tests -Our test suite is located in the `tests` folder. It is located outside of the sources as to not pollute distributions (it would be very wrong to publish a `tests` package as part of our distributions, since this name is extremely common), or worse, the public API. The `tests` folder is however included in our source distributions (`.tar.gz`), alongside most of our metadata and configuration files. Check out `pyproject.toml` to get the full list of files included in our source distributions. +Our test suite is located in the `tests` folder. It is located outside of the sources as to not pollute distributions (it would be very wrong to publish a `tests` package as part of our distributions, since this name is extremely common), or worse, the public API. The `tests` folder is however included in our source distributions (`.tar.gz`), alongside most of our metadata and configuration files. Check out our `pyproject.toml` files to get the full list of files included in our source distributions for every individual package within the `packages/` folder. The test suite is based on [pytest](https://docs.pytest.org/en/8.2.x/). Test modules reflect our internal API structure, and except for a few test modules that test specific aspects of our API, each test module tests the logic from the corresponding module in the internal API. For example, `test_finder.py` tests code of the `griffe._internal.finder` internal module, while `test_functions` tests our ability to extract correct information from function signatures, statically. The general rule of thumb when writing new tests is to mirror the internal API. If a test touches to many aspects of the loading process, it can be added to the `test_loader` test module. ## Program structure -The internal API is contained within the `src/griffe/_internal` folder. The top-level `griffe/__init__.py` module exposes all the public API, by importing the internal objects from various submodules of `griffe._internal`. +Griffe is split into two pieces: the library and the CLI. -Users then import `griffe` directly, or import objects from it. +Each of them has an internal API contained within an `_internal` folder: + +- `packages/griffelib/src/griffe/_internal` for the library, +- `packages/griffecli/src/griffecli/_internal` for the CLI. + +Griffe can be installed in library-only mode, which means that the CLI package from `packages/griffecli` is not present. +Library-only mode can be preferred if the user does not utilize the CLI functionality of Griffe and does not want to incorporate its dependencies. + +The top-level `packages/griffelib/src/griffe/__init__.py` module exposes all the public API available: it always re-exports internal objects from various submodules of `griffe._internal` and, if the CLI is installed, it re-exports the public API of `griffecli` as well. + +Users then import `griffe` directly, or import objects from it. If they don't have `griffecli` installed, they cannot import the CLI-related functionality, +such as [`griffecli.check`][]. We'll be honest: our code organization is not the most elegant, but it works :shrug: Have a look at the following module dependency graph, which will basically tell you nothing except that we have a lot of inter-module dependencies. Arrows read as "imports from". The code base is generally pleasant to work with though. @@ -122,7 +134,7 @@ if os.getenv("DEPLOY") == "true": from pydeps.target import Target cli.verbose = cli._not_verbose - options = cli.parse_args(["src/griffe", "--noshow", "--reverse"]) + options = cli.parse_args(["packages/griffelib/src/griffe", "--noshow", "--reverse"]) colors.START_COLOR = 128 target = Target(options["fname"]) with target.chdir_work(): diff --git a/docs/guide/contributors/workflow.md b/docs/guide/contributors/workflow.md index 62d2ff5f4..70fde1107 100644 --- a/docs/guide/contributors/workflow.md +++ b/docs/guide/contributors/workflow.md @@ -56,7 +56,11 @@ Deprecated code should also be marked as legacy code. We use [Yore](https://pawa Examples: -```python title="Remove function when we bump to 2.0" +```python title="Remove function when we bump to 5.0" +# YORE: Bump 5: Remove block. +def deprecated_function(): + ... +``` ```python title="Simplify imports when Python 3.15 is EOL" # YORE: EOL 3.15: Replace block with line 4. diff --git a/docs/installation.md b/docs/installation.md index 531dcab22..062b265d1 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -59,6 +59,67 @@ Griffe is a Python package, so you can install it with your favorite Python pack +## Install as a library only + +If you only need the library for API introspection and analysis without the CLI tool, you can install `griffelib`: + +=== ":simple-python: pip" + ```bash + pip install griffelib + ``` + +
+ + [pip](https://pip.pypa.io/en/stable/) is the main package installer for Python. + +
+ +=== ":simple-pdm: pdm" + ```bash + pdm add griffelib + ``` + +
+ + [PDM](https://pdm-project.org/en/latest/) is an all-in-one solution for Python project management. + +
+ +=== ":simple-poetry: poetry" + ```bash + poetry add griffelib + ``` + +
+ + [Poetry](https://python-poetry.org/) is an all-in-one solution for Python project management. + +
+ +=== ":simple-rye: rye" + ```bash + rye add griffelib + ``` + +
+ + [Rye](https://rye.astral.sh/) is an all-in-one solution for Python project management, written in Rust. + +
+ +=== ":simple-astral: uv" + ```bash + uv add griffelib + ``` + +
+ + [uv](https://docs.astral.sh/uv/) is an extremely fast Python package and project manager, written in Rust. + +
+ +This installs the `griffe` package as usual, but without the CLI program and its dependencies. + ## Install as a tool only === ":simple-python: pip" @@ -104,3 +165,17 @@ Griffe is a Python package, so you can install it with your favorite Python pack [uv](https://docs.astral.sh/uv/) is an extremely fast Python package and project manager, written in Rust. + +## Running Griffe + +Once installed, you can run Griffe using the `griffe` command: + +```console +$ griffe check mypackage +``` + +Or as a Python module: + +```console +$ python -m griffe check mypackage +``` diff --git a/docs/reference/api/cli.md b/docs/reference/api/cli.md index 08e83a31a..c806e83cf 100644 --- a/docs/reference/api/cli.md +++ b/docs/reference/api/cli.md @@ -2,12 +2,12 @@ ## **Main API** -::: griffe.main +::: griffecli.main -::: griffe.check +::: griffecli.check -::: griffe.dump +::: griffecli.dump ## **Advanced API** -::: griffe.get_parser +::: griffecli.get_parser diff --git a/docs/reference/api/loggers.md b/docs/reference/api/loggers.md index 0ba1d0dbd..afb2b220a 100644 --- a/docs/reference/api/loggers.md +++ b/docs/reference/api/loggers.md @@ -10,7 +10,7 @@ ::: griffe.LogLevel -::: griffe.DEFAULT_LOG_LEVEL +::: griffecli.DEFAULT_LOG_LEVEL options: annotations_path: full diff --git a/duties.py b/duties.py index d3f1178cd..ff244d2f7 100644 --- a/duties.py +++ b/duties.py @@ -24,7 +24,7 @@ from duty.context import Context -PY_SRC_PATHS = (Path(_) for _ in ("src", "tests", "duties.py", "scripts")) +PY_SRC_PATHS = (Path(_) for _ in ("packages/griffecli/src", "packages/griffelib/src", "tests", "duties.py", "scripts")) PY_SRC_LIST = tuple(str(_) for _ in PY_SRC_PATHS) PY_SRC = " ".join(PY_SRC_LIST) CI = os.environ.get("CI", "0") in {"1", "true", "yes", ""} @@ -287,14 +287,27 @@ def check_api(ctx: Context, *cli_args: str) -> None: ctx.run( tools.griffe.check( "griffe", - search=["src"], + search=["packages/griffelib/src"], color=True, extensions=[ "griffe_inherited_docstrings", "unpack_typeddict", ], ).add_args(*cli_args), - title="Checking for API breaking changes", + title="Checking for API breaking changes in Griffe library", + nofail=True, + ) + ctx.run( + tools.griffe.check( + "griffecli", + search=["packages/griffecli/src"], + color=True, + extensions=[ + "griffe_inherited_docstrings", + "unpack_typeddict", + ], + ).add_args(*cli_args), + title="Checking for API breaking changes in Griffe CLI", nofail=True, ) diff --git a/mkdocs.yml b/mkdocs.yml index 0ca192944..2c856afd0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -4,7 +4,7 @@ site_url: "https://mkdocstrings.github.io/griffe" repo_url: "https://github.com/mkdocstrings/griffe" repo_name: "mkdocstrings/griffe" site_dir: "site" -watch: [mkdocs.yml, README.md, CONTRIBUTING.md, CHANGELOG.md, src] +watch: [mkdocs.yml, README.md, CONTRIBUTING.md, CHANGELOG.md, packages] copyright: Copyright © 2021 Timothée Mazzucotelli edit_uri: edit/main/docs/ @@ -218,7 +218,7 @@ plugins: - url: https://docs.python.org/3/objects.inv domains: [std, py] - https://typing-extensions.readthedocs.io/en/latest/objects.inv - paths: [src, scripts, .] + paths: [packages/griffelib/src, packages/griffecli/src, scripts, .] options: backlinks: tree docstring_options: diff --git a/packages/griffecli/pyproject.toml b/packages/griffecli/pyproject.toml new file mode 100644 index 000000000..7cb4066ac --- /dev/null +++ b/packages/griffecli/pyproject.toml @@ -0,0 +1,60 @@ +[build-system] +# pdm-backend is left here as a dependency of the version discovery script currently in use. +# It may be removed in the future. See mkdocstrings/griffe#430 +requires = ["hatchling", "pdm-backend", "uv-dynamic-versioning>=0.7.0"] +build-backend = "hatchling.build" + +[project] +name = "griffecli" +description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." +authors = [{name = "Timothée Mazzucotelli", email = "dev@pawamoy.fr"}] +license = "ISC" +license-files = ["LICENSE"] +requires-python = ">=3.10" +keywords = ["api", "signature", "breaking-changes", "static-analysis", "dynamic-analysis"] +dynamic = ["version", "dependencies"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + # YORE: EOL 3.10: Remove line. + "Programming Language :: Python :: 3.10", + # YORE: EOL 3.11: Remove line. + "Programming Language :: Python :: 3.11", + # YORE: EOL 3.12: Remove line. + "Programming Language :: Python :: 3.12", + # YORE: EOL 3.13: Remove line. + "Programming Language :: Python :: 3.13", + # YORE: EOL 3.14: Remove line. + "Programming Language :: Python :: 3.14", + "Topic :: Documentation", + "Topic :: Software Development", + "Topic :: Software Development :: Documentation", + "Topic :: Utilities", + "Typing :: Typed", +] + +[project.scripts] +griffecli = "griffecli:main" + +[tool.hatch.version] +source = "code" +path = "../../scripts/get_version.py" +expression = "get_version()" + +[tool.hatch.build.targets.sdist.force-include] +"../../CHANGELOG.md" = "CHANGELOG.md" +"../../LICENSE" = "LICENSE" +"../../README.md" = "README.md" + +[tool.hatch.metadata.hooks.uv-dynamic-versioning] +# Dependencies are dynamically versioned; {{version}} is substituted at build time. +dependencies = ["griffelib=={{version}}", "colorama>=0.4"] + +[tool.hatch.build.targets.wheel] +packages = ["src/griffecli"] + +[tool.uv.sources] +griffelib = { workspace = true } diff --git a/packages/griffecli/src/griffecli/__init__.py b/packages/griffecli/src/griffecli/__init__.py new file mode 100644 index 000000000..b7c23fc12 --- /dev/null +++ b/packages/griffecli/src/griffecli/__init__.py @@ -0,0 +1,27 @@ +# This top-level module imports all public names from the CLI package, +# and exposes them as public objects. + +"""Griffe CLI package. + +The CLI (Command Line Interface) for the griffe library. +This package provides command-line tools for interacting with griffe. + +## CLI entrypoints + +- [`griffecli.main`][]: Run the main program. +- [`griffecli.check`][]: Check for API breaking changes in two versions of the same package. +- [`griffecli.dump`][]: Load packages data and dump it as JSON. +- [`griffecli.get_parser`][]: Get the argument parser for the CLI. +""" + +from __future__ import annotations + +from griffecli._internal.cli import DEFAULT_LOG_LEVEL, check, dump, get_parser, main + +__all__ = [ + "DEFAULT_LOG_LEVEL", + "check", + "dump", + "get_parser", + "main", +] diff --git a/src/griffe/__main__.py b/packages/griffecli/src/griffecli/__main__.py similarity index 71% rename from src/griffe/__main__.py rename to packages/griffecli/src/griffecli/__main__.py index a9047c9b9..22d5b7441 100644 --- a/src/griffe/__main__.py +++ b/packages/griffecli/src/griffecli/__main__.py @@ -1,4 +1,4 @@ -# Entry-point module, in case you use `python -m griffe`. +# Entry-point module, in case you use `python -m griffecli`. # # Why does this file exist, and why `__main__`? For more info, read: # @@ -7,7 +7,7 @@ import sys -from griffe._internal.cli import main +from griffecli._internal.cli import main if __name__ == "__main__": sys.exit(main(sys.argv[1:])) diff --git a/packages/griffecli/src/griffecli/_internal/__init__.py b/packages/griffecli/src/griffecli/_internal/__init__.py new file mode 100644 index 000000000..03d25ad8f --- /dev/null +++ b/packages/griffecli/src/griffecli/_internal/__init__.py @@ -0,0 +1 @@ +# Internal modules for the griffecli package. diff --git a/src/griffe/_internal/cli.py b/packages/griffecli/src/griffecli/_internal/cli.py similarity index 99% rename from src/griffe/_internal/cli.py rename to packages/griffecli/src/griffecli/_internal/cli.py index 1f6c767f5..5bd46fcc9 100644 --- a/src/griffe/_internal/cli.py +++ b/packages/griffecli/src/griffecli/_internal/cli.py @@ -4,11 +4,11 @@ # We might be tempted to import things from `__main__` later, # but that will cause problems; the code will get executed twice: # -# - When we run `python -m griffe`, Python will execute +# - When we run `python -m griffecli`, Python will execute # `__main__.py` as a script. That means there won't be any -# `griffe.__main__` in `sys.modules`. +# `griffecli.__main__` in `sys.modules`. # - When you import `__main__` it will get executed again (as a module) because -# there's no `griffe.__main__` in `sys.modules`. +# there's no `griffecli.__main__` in `sys.modules`. from __future__ import annotations diff --git a/packages/griffelib/pyproject.toml b/packages/griffelib/pyproject.toml new file mode 100644 index 000000000..67d9e1696 --- /dev/null +++ b/packages/griffelib/pyproject.toml @@ -0,0 +1,55 @@ +[build-system] +# pdm-backend is left here as a dependency of the version discovery script currently in use. +# It may be removed in the future. See mkdocstrings/griffe#430 +requires = ["hatchling", "pdm-backend", "uv-dynamic-versioning>=0.7.0"] +build-backend = "hatchling.build" + +[project] +name = "griffelib" +description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." +authors = [{name = "Timothée Mazzucotelli", email = "dev@pawamoy.fr"}] +license = "ISC" +license-files = ["LICENSE"] +requires-python = ">=3.10" +keywords = ["api", "signature", "breaking-changes", "static-analysis", "dynamic-analysis"] +dynamic = ["version"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + # YORE: EOL 3.10: Remove line. + "Programming Language :: Python :: 3.10", + # YORE: EOL 3.11: Remove line. + "Programming Language :: Python :: 3.11", + # YORE: EOL 3.12: Remove line. + "Programming Language :: Python :: 3.12", + # YORE: EOL 3.13: Remove line. + "Programming Language :: Python :: 3.13", + # YORE: EOL 3.14: Remove line. + "Programming Language :: Python :: 3.14", + "Topic :: Documentation", + "Topic :: Software Development", + "Topic :: Software Development :: Documentation", + "Topic :: Utilities", + "Typing :: Typed", +] + +[project.optional-dependencies] +# The 'pypi' extra provides dependencies needed for the load_pypi functionality +# to download and inspect packages from PyPI. +pypi = ["pip>=24.0", "platformdirs>=4.2", "wheel>=0.42"] + +[tool.hatch.version] +source = "code" +path = "../../scripts/get_version.py" +expression = "get_version()" + +[tool.hatch.build.targets.sdist.force-include] +"../../CHANGELOG.md" = "CHANGELOG.md" +"../../LICENSE" = "LICENSE" +"../../README.md" = "README.md" + +[tool.hatch.build.targets.wheel] +packages = ["src/griffe"] diff --git a/src/griffe/__init__.py b/packages/griffelib/src/griffe/__init__.py similarity index 95% rename from src/griffe/__init__.py rename to packages/griffelib/src/griffe/__init__.py index c06aad3a3..eef5da28b 100644 --- a/src/griffe/__init__.py +++ b/packages/griffelib/src/griffe/__init__.py @@ -2,13 +2,13 @@ # and exposes them as public objects. We have tests to make sure # no object is forgotten in this list. -"""Griffe package. +"""Griffe library package. Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API. -The entirety of the public API is exposed here, in the top-level `griffe` module. +The entirety of the library API is exposed here, in the top-level `griffe` module. All messages written to standard output or error are logged using the `logging` module. Our logger's name is set to `"griffe"` and is public (you can rely on it). @@ -20,14 +20,6 @@ The following paragraphs will help you discover the package's content. -## CLI entrypoints - -Griffe provides a command-line interface (CLI) to interact with the package. The CLI entrypoints can be called from Python code. - -- [`griffe.main`][]: Run the main program. -- [`griffe.check`][]: Check for API breaking changes in two versions of the same package. -- [`griffe.dump`][]: Load packages data and dump it as JSON. - ## Loaders To load API data, Griffe provides several high-level functions. @@ -185,7 +177,6 @@ from griffe._internal.agents.nodes.values import get_value, safe_get_value from griffe._internal.agents.visitor import Visitor, builtin_decorators, stdlib_decorators, typing_overload, visit from griffe._internal.c3linear import c3linear_merge -from griffe._internal.cli import DEFAULT_LOG_LEVEL, check, dump, get_parser, main from griffe._internal.collections import LinesCollection, ModulesCollection from griffe._internal.diff import ( AttributeChangedTypeBreakage, @@ -383,7 +374,6 @@ # names = sorted(n for n in dir(griffe) if not n.startswith("_") and n not in ("annotations",)) # print('__all__ = [\n "' + '",\n "'.join(names) + '",\n]') __all__ = [ - "DEFAULT_LOG_LEVEL", "Alias", "AliasResolutionError", "Attribute", @@ -546,9 +536,7 @@ "builtin_decorators", "builtin_extensions", "c3linear_merge", - "check", "docstring_warning", - "dump", "dynamic_import", "find_breaking_changes", "get__all__", @@ -563,7 +551,6 @@ "get_name", "get_names", "get_parameters", - "get_parser", "get_value", "htree", "infer_docstring_style", @@ -574,7 +561,6 @@ "load_git", "load_pypi", "logger", - "main", "merge_stubs", "module_vtree", "parse", @@ -605,3 +591,26 @@ "visit", "vtree", ] + +# Re-export griffecli for backward compatibility. +try: + from griffecli import * # noqa: F403 + from griffecli import __all__ as __cli_all__ +except ImportError: + # Keep this in sync with the exported members of griffecli. + _MISSING_FROM_GRIFFECLI = { + "DEFAULT_LOG_LEVEL", + "check", + "dump", + "get_parser", + "main", + } + + def __getattr__(attr: str) -> object: + if attr in _MISSING_FROM_GRIFFECLI: + raise ImportError(f"Please install `griffecli` to use {'griffe.' + attr!r}") + raise AttributeError(attr) +else: + __all__ += __cli_all__ # noqa: PLE0605 + # Ignore any re-exported API in internal API tests. + _REEXPORTED_EXTERNAL_API = set(__cli_all__) diff --git a/packages/griffelib/src/griffe/__main__.py b/packages/griffelib/src/griffe/__main__.py new file mode 100644 index 000000000..e3bb8a89f --- /dev/null +++ b/packages/griffelib/src/griffe/__main__.py @@ -0,0 +1,20 @@ +"""Entry-point module, in case you use `python -m griffe`. + +Why does this file exist, and why `__main__`? For more info, read: + +- https://www.python.org/dev/peps/pep-0338/ +- https://docs.python.org/3/using/cmdline.html#cmdoption-m +""" + +import sys + +try: + from griffecli import main +except ModuleNotFoundError as exc: + raise ModuleNotFoundError( + "`griffecli` or its dependencies are not installed. " + "Install `griffecli` to use `python -m griffe`.", + ) from exc + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/src/griffe/_internal/__init__.py b/packages/griffelib/src/griffe/_internal/__init__.py similarity index 100% rename from src/griffe/_internal/__init__.py rename to packages/griffelib/src/griffe/_internal/__init__.py diff --git a/src/griffe/_internal/agents/__init__.py b/packages/griffelib/src/griffe/_internal/agents/__init__.py similarity index 100% rename from src/griffe/_internal/agents/__init__.py rename to packages/griffelib/src/griffe/_internal/agents/__init__.py diff --git a/src/griffe/_internal/agents/inspector.py b/packages/griffelib/src/griffe/_internal/agents/inspector.py similarity index 100% rename from src/griffe/_internal/agents/inspector.py rename to packages/griffelib/src/griffe/_internal/agents/inspector.py diff --git a/src/griffe/_internal/agents/nodes/__init__.py b/packages/griffelib/src/griffe/_internal/agents/nodes/__init__.py similarity index 100% rename from src/griffe/_internal/agents/nodes/__init__.py rename to packages/griffelib/src/griffe/_internal/agents/nodes/__init__.py diff --git a/src/griffe/_internal/agents/nodes/assignments.py b/packages/griffelib/src/griffe/_internal/agents/nodes/assignments.py similarity index 100% rename from src/griffe/_internal/agents/nodes/assignments.py rename to packages/griffelib/src/griffe/_internal/agents/nodes/assignments.py diff --git a/src/griffe/_internal/agents/nodes/ast.py b/packages/griffelib/src/griffe/_internal/agents/nodes/ast.py similarity index 100% rename from src/griffe/_internal/agents/nodes/ast.py rename to packages/griffelib/src/griffe/_internal/agents/nodes/ast.py diff --git a/src/griffe/_internal/agents/nodes/docstrings.py b/packages/griffelib/src/griffe/_internal/agents/nodes/docstrings.py similarity index 100% rename from src/griffe/_internal/agents/nodes/docstrings.py rename to packages/griffelib/src/griffe/_internal/agents/nodes/docstrings.py diff --git a/src/griffe/_internal/agents/nodes/exports.py b/packages/griffelib/src/griffe/_internal/agents/nodes/exports.py similarity index 100% rename from src/griffe/_internal/agents/nodes/exports.py rename to packages/griffelib/src/griffe/_internal/agents/nodes/exports.py diff --git a/src/griffe/_internal/agents/nodes/imports.py b/packages/griffelib/src/griffe/_internal/agents/nodes/imports.py similarity index 100% rename from src/griffe/_internal/agents/nodes/imports.py rename to packages/griffelib/src/griffe/_internal/agents/nodes/imports.py diff --git a/src/griffe/_internal/agents/nodes/parameters.py b/packages/griffelib/src/griffe/_internal/agents/nodes/parameters.py similarity index 100% rename from src/griffe/_internal/agents/nodes/parameters.py rename to packages/griffelib/src/griffe/_internal/agents/nodes/parameters.py diff --git a/src/griffe/_internal/agents/nodes/runtime.py b/packages/griffelib/src/griffe/_internal/agents/nodes/runtime.py similarity index 100% rename from src/griffe/_internal/agents/nodes/runtime.py rename to packages/griffelib/src/griffe/_internal/agents/nodes/runtime.py diff --git a/src/griffe/_internal/agents/nodes/values.py b/packages/griffelib/src/griffe/_internal/agents/nodes/values.py similarity index 100% rename from src/griffe/_internal/agents/nodes/values.py rename to packages/griffelib/src/griffe/_internal/agents/nodes/values.py diff --git a/src/griffe/_internal/agents/visitor.py b/packages/griffelib/src/griffe/_internal/agents/visitor.py similarity index 100% rename from src/griffe/_internal/agents/visitor.py rename to packages/griffelib/src/griffe/_internal/agents/visitor.py diff --git a/src/griffe/_internal/c3linear.py b/packages/griffelib/src/griffe/_internal/c3linear.py similarity index 100% rename from src/griffe/_internal/c3linear.py rename to packages/griffelib/src/griffe/_internal/c3linear.py diff --git a/src/griffe/_internal/collections.py b/packages/griffelib/src/griffe/_internal/collections.py similarity index 100% rename from src/griffe/_internal/collections.py rename to packages/griffelib/src/griffe/_internal/collections.py diff --git a/src/griffe/_internal/debug.py b/packages/griffelib/src/griffe/_internal/debug.py similarity index 97% rename from src/griffe/_internal/debug.py rename to packages/griffelib/src/griffe/_internal/debug.py index 8f3ca86d3..aaa07ac50 100644 --- a/src/griffe/_internal/debug.py +++ b/packages/griffelib/src/griffe/_internal/debug.py @@ -62,7 +62,7 @@ def _interpreter_name_version() -> tuple[str, str]: return "", "0.0.0" -def _get_version(dist: str = "griffe") -> str: +def _get_version(dist: str = "griffelib") -> str: """Get version of the given distribution. Parameters: @@ -84,7 +84,7 @@ def _get_debug_info() -> _Environment: Environment information. """ py_name, py_version = _interpreter_name_version() - packages = ["griffe"] + packages = ["griffelib", "griffe"] variables = ["PYTHONPATH", *[var for var in os.environ if var.startswith("GRIFFE")]] return _Environment( interpreter_name=py_name, diff --git a/src/griffe/_internal/diff.py b/packages/griffelib/src/griffe/_internal/diff.py similarity index 100% rename from src/griffe/_internal/diff.py rename to packages/griffelib/src/griffe/_internal/diff.py diff --git a/src/griffe/_internal/docstrings/__init__.py b/packages/griffelib/src/griffe/_internal/docstrings/__init__.py similarity index 100% rename from src/griffe/_internal/docstrings/__init__.py rename to packages/griffelib/src/griffe/_internal/docstrings/__init__.py diff --git a/src/griffe/_internal/docstrings/auto.py b/packages/griffelib/src/griffe/_internal/docstrings/auto.py similarity index 100% rename from src/griffe/_internal/docstrings/auto.py rename to packages/griffelib/src/griffe/_internal/docstrings/auto.py diff --git a/src/griffe/_internal/docstrings/google.py b/packages/griffelib/src/griffe/_internal/docstrings/google.py similarity index 100% rename from src/griffe/_internal/docstrings/google.py rename to packages/griffelib/src/griffe/_internal/docstrings/google.py diff --git a/src/griffe/_internal/docstrings/models.py b/packages/griffelib/src/griffe/_internal/docstrings/models.py similarity index 100% rename from src/griffe/_internal/docstrings/models.py rename to packages/griffelib/src/griffe/_internal/docstrings/models.py diff --git a/src/griffe/_internal/docstrings/numpy.py b/packages/griffelib/src/griffe/_internal/docstrings/numpy.py similarity index 100% rename from src/griffe/_internal/docstrings/numpy.py rename to packages/griffelib/src/griffe/_internal/docstrings/numpy.py diff --git a/src/griffe/_internal/docstrings/parsers.py b/packages/griffelib/src/griffe/_internal/docstrings/parsers.py similarity index 100% rename from src/griffe/_internal/docstrings/parsers.py rename to packages/griffelib/src/griffe/_internal/docstrings/parsers.py diff --git a/src/griffe/_internal/docstrings/sphinx.py b/packages/griffelib/src/griffe/_internal/docstrings/sphinx.py similarity index 100% rename from src/griffe/_internal/docstrings/sphinx.py rename to packages/griffelib/src/griffe/_internal/docstrings/sphinx.py diff --git a/src/griffe/_internal/docstrings/utils.py b/packages/griffelib/src/griffe/_internal/docstrings/utils.py similarity index 100% rename from src/griffe/_internal/docstrings/utils.py rename to packages/griffelib/src/griffe/_internal/docstrings/utils.py diff --git a/src/griffe/_internal/encoders.py b/packages/griffelib/src/griffe/_internal/encoders.py similarity index 100% rename from src/griffe/_internal/encoders.py rename to packages/griffelib/src/griffe/_internal/encoders.py diff --git a/src/griffe/_internal/enumerations.py b/packages/griffelib/src/griffe/_internal/enumerations.py similarity index 100% rename from src/griffe/_internal/enumerations.py rename to packages/griffelib/src/griffe/_internal/enumerations.py diff --git a/src/griffe/_internal/exceptions.py b/packages/griffelib/src/griffe/_internal/exceptions.py similarity index 100% rename from src/griffe/_internal/exceptions.py rename to packages/griffelib/src/griffe/_internal/exceptions.py diff --git a/src/griffe/_internal/expressions.py b/packages/griffelib/src/griffe/_internal/expressions.py similarity index 100% rename from src/griffe/_internal/expressions.py rename to packages/griffelib/src/griffe/_internal/expressions.py diff --git a/src/griffe/_internal/extensions/__init__.py b/packages/griffelib/src/griffe/_internal/extensions/__init__.py similarity index 100% rename from src/griffe/_internal/extensions/__init__.py rename to packages/griffelib/src/griffe/_internal/extensions/__init__.py diff --git a/src/griffe/_internal/extensions/base.py b/packages/griffelib/src/griffe/_internal/extensions/base.py similarity index 100% rename from src/griffe/_internal/extensions/base.py rename to packages/griffelib/src/griffe/_internal/extensions/base.py diff --git a/src/griffe/_internal/extensions/dataclasses.py b/packages/griffelib/src/griffe/_internal/extensions/dataclasses.py similarity index 100% rename from src/griffe/_internal/extensions/dataclasses.py rename to packages/griffelib/src/griffe/_internal/extensions/dataclasses.py diff --git a/src/griffe/_internal/extensions/unpack_typeddict.py b/packages/griffelib/src/griffe/_internal/extensions/unpack_typeddict.py similarity index 100% rename from src/griffe/_internal/extensions/unpack_typeddict.py rename to packages/griffelib/src/griffe/_internal/extensions/unpack_typeddict.py diff --git a/src/griffe/_internal/finder.py b/packages/griffelib/src/griffe/_internal/finder.py similarity index 97% rename from src/griffe/_internal/finder.py rename to packages/griffelib/src/griffe/_internal/finder.py index 23bc3be29..951d299a8 100644 --- a/src/griffe/_internal/finder.py +++ b/packages/griffelib/src/griffe/_internal/finder.py @@ -9,8 +9,8 @@ # ModuleSpec( # name='griffe.agents', # loader=<_frozen_importlib_external.SourceFileLoader object at 0x7fa5f34e8110>, -# origin='/media/data/dev/griffe/src/griffe/agents/__init__.py', -# submodule_search_locations=['/media/data/dev/griffe/src/griffe/agents'], +# origin='/media/data/dev/griffelib/packages/griffe/src/griffe/agents/__init__.py', +# submodule_search_locations=['/media/data/dev/griffelib/packages/griffe/src/griffe/agents'], # ) # ``` @@ -470,9 +470,9 @@ def _handle_pth_file(path: Path) -> list[_SP]: def _handle_editable_module(path: Path) -> list[_SP]: if _match_pattern(path.name, (*_editable_editables_patterns, *_editable_scikit_build_core_patterns)): # Support for how 'editables' write these files: - # example line: `F.map_module('griffe', '/media/data/dev/griffe/src/griffe/__init__.py')`. + # example line: `F.map_module('pkg', '/data/dev/pkg/src/pkg/__init__.py')`. # And how 'scikit-build-core' writes these files: - # example line: `install({'griffe': '/media/data/dev/griffe/src/griffe/__init__.py'}, {'cmake_example': ...}, None, False, True)`. + # example line: `install({'pkg': '/data/dev/pkg/src/pkg/__init__.py'}, {'cmake_example': ...}, None, False, True)`. try: editable_lines = path.read_text(encoding="utf-8-sig").strip().splitlines(keepends=False) except FileNotFoundError as error: @@ -483,7 +483,7 @@ def _handle_editable_module(path: Path) -> list[_SP]: return [_SP(new_path)] if _match_pattern(path.name, _editable_setuptools_patterns): # Support for how 'setuptools' writes these files: - # example line: `MAPPING = {'griffe': '/media/data/dev/griffe/src/griffe', 'briffe': '/media/data/dev/griffe/src/briffe'}`. + # example line: `MAPPING = {'pkg': '/data/dev/pkg/src/pkg', 'pkg2': '/data/dev/pkg/src/pkg2'}`. # with annotation: `MAPPING: dict[str, str] = {...}`. parsed_module = ast.parse(path.read_text(encoding="utf8")) for node in parsed_module.body: @@ -508,7 +508,7 @@ def _handle_editable_module(path: Path) -> list[_SP]: and node.value.func.id == "install" and isinstance(node.value.args[1], ast.Constant) ): - build_path = Path(node.value.args[1].value, "src") # type: ignore[arg-type] + build_path = Path(node.value.args[1].value, "packages/griffelib/src") # type: ignore[arg-type] # NOTE: What if there are multiple packages? pkg_name = next(build_path.iterdir()).name return [_SP(build_path, always_scan_for=pkg_name)] diff --git a/src/griffe/_internal/git.py b/packages/griffelib/src/griffe/_internal/git.py similarity index 100% rename from src/griffe/_internal/git.py rename to packages/griffelib/src/griffe/_internal/git.py diff --git a/src/griffe/_internal/importer.py b/packages/griffelib/src/griffe/_internal/importer.py similarity index 100% rename from src/griffe/_internal/importer.py rename to packages/griffelib/src/griffe/_internal/importer.py diff --git a/src/griffe/_internal/loader.py b/packages/griffelib/src/griffe/_internal/loader.py similarity index 100% rename from src/griffe/_internal/loader.py rename to packages/griffelib/src/griffe/_internal/loader.py diff --git a/src/griffe/_internal/logger.py b/packages/griffelib/src/griffe/_internal/logger.py similarity index 100% rename from src/griffe/_internal/logger.py rename to packages/griffelib/src/griffe/_internal/logger.py diff --git a/src/griffe/_internal/merger.py b/packages/griffelib/src/griffe/_internal/merger.py similarity index 100% rename from src/griffe/_internal/merger.py rename to packages/griffelib/src/griffe/_internal/merger.py diff --git a/src/griffe/_internal/mixins.py b/packages/griffelib/src/griffe/_internal/mixins.py similarity index 100% rename from src/griffe/_internal/mixins.py rename to packages/griffelib/src/griffe/_internal/mixins.py diff --git a/src/griffe/_internal/models.py b/packages/griffelib/src/griffe/_internal/models.py similarity index 100% rename from src/griffe/_internal/models.py rename to packages/griffelib/src/griffe/_internal/models.py diff --git a/src/griffe/_internal/py.typed b/packages/griffelib/src/griffe/_internal/py.typed similarity index 100% rename from src/griffe/_internal/py.typed rename to packages/griffelib/src/griffe/_internal/py.typed diff --git a/src/griffe/_internal/stats.py b/packages/griffelib/src/griffe/_internal/stats.py similarity index 100% rename from src/griffe/_internal/stats.py rename to packages/griffelib/src/griffe/_internal/stats.py diff --git a/src/griffe/_internal/tests.py b/packages/griffelib/src/griffe/_internal/tests.py similarity index 100% rename from src/griffe/_internal/tests.py rename to packages/griffelib/src/griffe/_internal/tests.py diff --git a/pyproject.toml b/pyproject.toml index f33dc1fc7..2eabbee53 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [build-system] # pdm-backend is left here as a dependency of the version discovery script currently in use. # It may be removed in the future. See mkdocstrings/griffe#430 -requires = ["hatchling", "pdm-backend"] +requires = ["hatchling", "pdm-backend", "uv-dynamic-versioning>=0.7.0"] build-backend = "hatchling.build" [project] @@ -13,7 +13,7 @@ license-files = ["LICENSE"] readme = "README.md" requires-python = ">=3.10" keywords = ["api", "signature", "breaking-changes", "static-analysis", "dynamic-analysis"] -dynamic = ["version"] +dynamic = ["version", "dependencies", "optional-dependencies"] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", @@ -36,16 +36,6 @@ classifiers = [ "Topic :: Utilities", "Typing :: Typed", ] -dependencies = [ - "colorama>=0.4", -] - -[project.optional-dependencies] -pypi = [ - "pip>=24.0", - "platformdirs>=4.2", - "wheel>=0.42", -] [project.urls] Homepage = "https://mkdocstrings.github.io/griffe" @@ -58,13 +48,22 @@ Gitter = "https://gitter.im/mkdocstrings/griffe" Funding = "https://github.com/sponsors/pawamoy" [project.scripts] -griffe = "griffe:main" +griffe = "griffecli:main" [tool.hatch.version] source = "code" path = "scripts/get_version.py" expression = "get_version()" +[tool.hatch.metadata.hooks.uv-dynamic-versioning] +# Dependencies are dynamically versioned; {{version}} is substituted at build time. +# This ensures griffe, griffelib, and griffecli versions are always 1:1. +dependencies = ["griffelib=={{version}}", "griffecli=={{version}}"] + +[tool.hatch.metadata.hooks.uv-dynamic-versioning.optional-dependencies] +# The 'pypi' extra re-exports griffelib[pypi] for backward compatibility. +pypi = ["griffelib[pypi]=={{version}}"] + [tool.hatch.build] # Include as much as possible in the source distribution, to help redistributors. ignore-vcs = true @@ -75,7 +74,6 @@ include = [ "config", "docs", "scripts", - "src", "share", "tests", "duties.py", @@ -88,7 +86,7 @@ include = [ # Manual pages can be included in the wheel. # Depending on the installation tool, they will be accessible to users. # pipx supports it, uv does not yet, see https://github.com/astral-sh/uv/issues/4731. -sources = ["src/"] +bypass-selection = true artifacts = ["share/**/*"] [dependency-groups] @@ -136,3 +134,8 @@ ci = [ [tool.uv] default-groups = ["maintain", "ci", "docs"] +workspace = { members = ["packages/*"] } + +[tool.uv.sources] +griffelib = { workspace = true } +griffecli = { workspace = true } diff --git a/scripts/gen_structure_docs.py b/scripts/gen_structure_docs.py index a33860efe..85d07a000 100644 --- a/scripts/gen_structure_docs.py +++ b/scripts/gen_structure_docs.py @@ -68,7 +68,7 @@ def render_internal_api(heading_level: int = 4) -> None: heading_level: The initial level of Markdown headings. """ root = Path(os.environ["MKDOCS_CONFIG_DIR"]) - src = root / "src" + src = root / "packages" / "griffelib" / "src" internal_api = src / "griffe" / "_internal" print(_comment_block(internal_api / "__init__.py")) _render_api(internal_api, internal_api, heading_level) @@ -81,7 +81,7 @@ def render_public_api(heading_level: int = 4) -> None: heading_level: The initial level of Markdown headings. """ root = Path(os.environ["MKDOCS_CONFIG_DIR"]) - src = root / "src" + src = root / "packages" / "griffelib" / "src" public_api = src / "griffe" print(f"{'#' * heading_level} `griffe`\n") print(_comment_block(public_api / "__init__.py")) @@ -94,7 +94,7 @@ def render_entrypoint(heading_level: int = 4) -> None: heading_level: The initial level of Markdown headings. """ root = Path(os.environ["MKDOCS_CONFIG_DIR"]) - src = root / "src" + src = root / "packages" / "griffelib" / "src" public_api = src / "griffe" print(f"{'#' * heading_level} `griffe.__main__`\n") print(_comment_block(public_api / "__main__.py")) diff --git a/src/griffe/py.typed b/src/griffe/py.typed deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/test_api.py b/tests/test_api.py index 0cd72b0a5..0cf2201b8 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -11,32 +11,53 @@ from mkdocstrings import Inventory import griffe +import griffecli if TYPE_CHECKING: - from collections.abc import Iterator + from collections.abc import Iterable, Iterator + from types import ModuleType -@pytest.fixture(name="loader", scope="module") -def _fixture_loader() -> griffe.GriffeLoader: +TESTED_MODULES = (griffe, griffecli) +_test_all_modules = pytest.mark.parametrize("tested_module", TESTED_MODULES) + + +@pytest.fixture(name="inventory", scope="module") +def _fixture_inventory() -> Inventory: + inventory_file = Path(__file__).parent.parent / "site" / "objects.inv" + if not inventory_file.exists(): + pytest.skip("The objects inventory is not available.") # ty: ignore[call-non-callable] + with inventory_file.open("rb") as file: + return Inventory.parse_sphinx(file) + + +def _load_modules(*modules: ModuleType) -> griffe.GriffeLoader: loader = griffe.GriffeLoader( extensions=griffe.load_extensions( "griffe_inherited_docstrings", "unpack_typeddict", ), ) - loader.load("griffe") + for module in modules: + loader.load(module.__name__) loader.resolve_aliases() return loader -@pytest.fixture(name="internal_api", scope="module") -def _fixture_internal_api(loader: griffe.GriffeLoader) -> griffe.Module: - return loader.modules_collection["griffe._internal"] +def _get_internal_api(module: ModuleType, loader: griffe.GriffeLoader | None = None) -> griffe.Module: + if loader is None: + loader = _load_modules(module) + return loader.modules_collection[module.__name__ + "._internal"] -@pytest.fixture(name="public_api", scope="module") -def _fixture_public_api(loader: griffe.GriffeLoader) -> griffe.Module: - return loader.modules_collection["griffe"] +def _get_reexported_names(module: ModuleType) -> Iterable[str]: + return getattr(module, "_REEXPORTED_EXTERNAL_API", ()) + + +def _get_public_api(module: ModuleType, loader: griffe.GriffeLoader | None = None) -> griffe.Module: + if loader is None: + loader = _load_modules(module) + return loader.modules_collection[module.__name__] def _yield_public_objects( @@ -76,32 +97,17 @@ def _yield_public_objects( continue -@pytest.fixture(name="modulelevel_internal_objects", scope="module") -def _fixture_modulelevel_internal_objects(internal_api: griffe.Module) -> list[griffe.Object | griffe.Alias]: +def _get_modulelevel_internal_objects(internal_api: griffe.Module) -> list[griffe.Object | griffe.Alias]: return list(_yield_public_objects(internal_api, modulelevel=True)) -@pytest.fixture(name="internal_objects", scope="module") -def _fixture_internal_objects(internal_api: griffe.Module) -> list[griffe.Object | griffe.Alias]: - return list(_yield_public_objects(internal_api, modulelevel=False, special=True)) - - -@pytest.fixture(name="public_objects", scope="module") -def _fixture_public_objects(public_api: griffe.Module) -> list[griffe.Object | griffe.Alias]: +def _get_public_objects(public_api: griffe.Module) -> list[griffe.Object | griffe.Alias]: return list(_yield_public_objects(public_api, modulelevel=False, inherited=True, special=True)) -@pytest.fixture(name="inventory", scope="module") -def _fixture_inventory() -> Inventory: - inventory_file = Path(__file__).parent.parent / "site" / "objects.inv" - if not inventory_file.exists(): - pytest.skip("The objects inventory is not available.") # ty: ignore[call-non-callable] - with inventory_file.open("rb") as file: - return Inventory.parse_sphinx(file) - - -def test_alias_proxies(internal_api: griffe.Module) -> None: +def test_alias_proxies() -> None: """The Alias class has all the necessary methods and properties.""" + internal_api = _get_internal_api(griffe) alias_members = set(internal_api["models.Alias"].all_members.keys()) for cls in ( internal_api["models.Module"], @@ -114,18 +120,22 @@ def test_alias_proxies(internal_api: griffe.Module) -> None: assert name in alias_members -def test_exposed_objects(modulelevel_internal_objects: list[griffe.Object | griffe.Alias]) -> None: +@_test_all_modules +def test_exposed_objects(tested_module: ModuleType) -> None: """All public objects in the internal API are exposed under `griffe`.""" + modulelevel_internal_objects = _get_modulelevel_internal_objects(_get_internal_api(tested_module)) not_exposed = [ obj.path for obj in modulelevel_internal_objects - if obj.name not in griffe.__all__ or not hasattr(griffe, obj.name) + if obj.name not in tested_module.__all__ or not hasattr(tested_module, obj.name) ] assert not not_exposed, "Objects not exposed:\n" + "\n".join(sorted(not_exposed)) -def test_unique_names(modulelevel_internal_objects: list[griffe.Object | griffe.Alias]) -> None: +@_test_all_modules +def test_unique_names(tested_module: ModuleType) -> None: """All internal objects have unique names.""" + modulelevel_internal_objects = _get_modulelevel_internal_objects(_get_public_api(tested_module)) names_to_paths = defaultdict(list) for obj in modulelevel_internal_objects: names_to_paths[obj.name].append(obj.path) @@ -133,14 +143,16 @@ def test_unique_names(modulelevel_internal_objects: list[griffe.Object | griffe. assert not non_unique, "Non-unique names:\n" + "\n".join(str(paths) for paths in non_unique) -def test_single_locations(public_api: griffe.Module) -> None: +@_test_all_modules +def test_single_locations(tested_module: ModuleType) -> None: """All objects have a single public location.""" def _public_path(obj: griffe.Object | griffe.Alias) -> bool: return obj.is_public and (obj.parent is None or _public_path(obj.parent)) + public_api = _get_public_api(tested_module) multiple_locations = {} - for obj_name in griffe.__all__: + for obj_name in set(tested_module.__all__).difference(_get_reexported_names(tested_module)): obj = public_api[obj_name] if obj.aliases and ( public_aliases := [path for path, alias in obj.aliases.items() if path != obj.path and _public_path(alias)] @@ -151,10 +163,15 @@ def _public_path(obj: griffe.Object | griffe.Alias) -> bool: ) -def test_api_matches_inventory(inventory: Inventory, public_objects: list[griffe.Object | griffe.Alias]) -> None: +@_test_all_modules +def test_api_matches_inventory(inventory: Inventory, tested_module: ModuleType) -> None: """All public objects are added to the inventory.""" ignore_names = {"__getattr__", "__init__", "__repr__", "__str__", "__post_init__"} + ignore_names.update(_get_reexported_names(tested_module)) ignore_paths = {"griffe.DataclassesExtension.*", "griffe.UnpackTypedDictExtension.*"} + loader = _load_modules(tested_module) + public_api = _get_public_api(tested_module, loader=loader) + public_objects = _get_public_objects(public_api) not_in_inventory = [ f"{obj.relative_filepath}:{obj.lineno}: {obj.path}" for obj in public_objects @@ -168,30 +185,38 @@ def test_api_matches_inventory(inventory: Inventory, public_objects: list[griffe assert not not_in_inventory, msg.format(paths="\n".join(sorted(not_in_inventory))) -def test_inventory_matches_api( - inventory: Inventory, - public_objects: list[griffe.Object | griffe.Alias], - loader: griffe.GriffeLoader, -) -> None: +def test_inventory_matches_api(inventory: Inventory) -> None: """The inventory doesn't contain any additional Python object.""" + loader = _load_modules(*TESTED_MODULES) not_in_api = [] - public_api_paths = {obj.path for obj in public_objects} - public_api_paths.add("griffe") + public_objects = [] + public_api_paths = set() + + for tested_module in TESTED_MODULES: + public_api = _get_public_api(tested_module, loader=loader) + module_public_objects = _get_public_objects(public_api) + public_api_paths.add(tested_module.__name__) + public_api_paths.update({obj.path for obj in module_public_objects}) + public_objects.extend(module_public_objects) + for item in inventory.values(): if item.domain == "py" and "(" not in item.name: obj = loader.modules_collection[item.name] if obj.path not in public_api_paths and not any(path in public_api_paths for path in obj.aliases): not_in_api.append(item.name) + msg = "Inventory objects not in public API (try running `make run mkdocs build`):\n{paths}" assert not not_in_api, msg.format(paths="\n".join(sorted(not_in_api))) -def test_no_module_docstrings_in_internal_api(internal_api: griffe.Module) -> None: +@_test_all_modules +def test_no_module_docstrings_in_internal_api(tested_module: ModuleType) -> None: """No module docstrings should be written in our internal API. The reasoning is that docstrings are addressed to users of the public API, but internal modules are not exposed to users, so they should not have docstrings. """ + internal_api = _get_internal_api(tested_module) def _modules(obj: griffe.Module) -> Iterator[griffe.Module]: for member in obj.modules.values(): diff --git a/tests/test_cli.py b/tests/test_cli.py index a71507a76..ae2d5b576 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -6,7 +6,8 @@ import pytest -from griffe._internal import cli, debug +from griffe._internal import debug +from griffecli._internal import cli def test_main() -> None: diff --git a/tests/test_finder.py b/tests/test_finder.py index 9b6d34076..26b6e81c6 100644 --- a/tests/test_finder.py +++ b/tests/test_finder.py @@ -125,9 +125,9 @@ def test_editables_file_handling(tmp_path: Path, editable_file_name: str) -> Non tmp_path: Pytest fixture. """ pth_file = tmp_path / editable_file_name - pth_file.write_text("hello\nF.map_module('griffe', 'src/griffe/__init__.py')", encoding="utf8") + pth_file.write_text("hello\nF.map_module('griffe', 'packages/griffelib/src/griffe/__init__.py')", encoding="utf8") paths = [sp.path for sp in _handle_editable_module(pth_file)] - assert paths == [Path("src")] + assert paths == [Path("packages/griffelib/src")] @pytest.mark.parametrize("annotation", ["", ": dict[str, str]"]) @@ -197,7 +197,7 @@ def test_meson_python_file_handling(tmp_path: Path) -> None: search_paths = _handle_editable_module(pth_file) assert all(sp.always_scan_for == "griffe" for sp in search_paths) paths = [sp.path for sp in search_paths] - assert paths == [Path("src")] + assert paths == [Path("packages/griffelib/src")] @pytest.mark.parametrize(