You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/guide/users/recommendations/public-apis.md
+13-15Lines changed: 13 additions & 15 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -32,7 +32,7 @@ Other components *could* be considered for the public API, but usually require t
32
32
33
33
Besides, logging and exception messages simply cannot allow deprecation periods where both old and new messages are emitted. Maintainers could however consider adding unique, short codes to messages for more robust consumption.
34
34
35
-
> GRIFFE: **Our recommendation — Communicate your public API, verify what you can.**
35
+
> GRIFFE: **Our recommendation — Communicate your public API, verify what you can.**
36
36
> Take the time to learn about and use ways to declare, communicate and deprecate your public API. Your users will have an easier time using your library. On the maintenance side, you won't get bug reports for uses that are not supported, or you will be able to quickly close them by pointing at the documentation explaining what your public API is, or why something was deprecated, for how long, and how to upgrade.
37
37
>
38
38
> Automate verifications around your public API with tools like Griffe. Currently Griffe doesn't support checking CLI configuration options, logger names or raised exceptions. If you have the capacity to, verify these manually before each release. [Griffe checks](../checking.md) and [API rules enforcement](#ensuring-api-rules) are a very good starting point.
@@ -123,7 +123,7 @@ Note that the wildcard imports logic stays the same, and imports either all obje
123
123
124
124
---
125
125
126
-
> GRIFFE: **Our recommendation — Use the underscore prefix and `__all__` conventions.**
126
+
> GRIFFE: **Our recommendation — Use the underscore prefix and `__all__` conventions.**
127
127
> Use both the underscore prefix convention for consistent naming at module and class levels, and the `__all__` convention for declaring your public API. We do not recommend using the redundant aliases convention, because it doesn't provide any information at runtime. We do not recommend the wildcard import convention either, for the same reason and [for additional reasons mentioned here](python-code.md#avoid-wildcard-imports). We still provide the [`griffe-public-redundant-aliases`](https://mkdocstrings.github.io/griffe-public-redundant-aliases/) and [`griffe-public-wildcard-imports`](https://mkdocstrings.github.io/griffe-public-wildcard-imports/) extensions for those who would still like to rely on these conventions.
@@ -132,7 +132,7 @@ Note that the wildcard imports logic stays the same, and imports either all obje
132
132
>
133
133
> > Even with `__all__` set appropriately, internal interfaces (packages, modules, classes, functions, attributes or other names) should still be prefixed with a single leading underscore.
134
134
135
-
> TIP: **Concatenating `__all__` for easier maintenance of `__init__` modules.**
135
+
> TIP: **Concatenating `__all__` for easier maintenance of `__init__` modules.**
136
136
> If you worry about maintenance of your `__init__` modules, know that you can very well concatenate `__all__` lists from submodules into the current one:
137
137
>
138
138
> ```tree
@@ -168,7 +168,7 @@ Note that the wildcard imports logic stays the same, and imports either all obje
168
168
>
169
169
> # Griffe supports the `+` and `+=` operators too:
> However we would argue that `this1a`, `that1a`, `this2a` and `that2a` should not be exposed publicly in more than one location. See our section on [unique names and public locations](#unique-names-and-public-locations).
@@ -279,10 +279,10 @@ If you don't want to bother prefixing every module with an underscore, you could
279
279
280
280
Whatever *hidden* layout you choose (private modules, internals, private package), it is not very important, as you will be able to switch from one to another easily. In Griffe we chose to experiment and go with the private package approach. This highlighted a few shortcomings that we were able to address in both Griffe and mkdocstrings-python. We later moved the private package under an internal folder.
281
281
282
-
WARNING: **Top-level-only exposition doesn't play well with large packages.**
282
+
WARNING: **Top-level-only exposition doesn't play well with large packages.**
283
283
The *fully* hidden layout plays well with small to medium projects. If you maintain a large project, it can become very impractical for both you and your users to expose every single object in the top-level `__init__` module. For large projects, it therefore makes sense to keep at least one or two additional public layers in your module layout. Sometimes packages also implement many variations of the same abstract class, using the same name in many different modules: in these cases, the modules are effective namespaces that could be kept in the public API.
284
284
285
-
GRIFFE: **Our recommendation — Hide your module layout early.**
285
+
GRIFFE: **Our recommendation — Hide your module layout early.**
286
286
Start hiding your module layout early! It is much easier to (partially) expose the layout later than to hide it after your users started relying on it. It will also make code reorganizations much easier.
287
287
288
288
## Unique names and public locations
@@ -349,7 +349,7 @@ my_package/
349
349
350
350
It feels weird to "unpublicize" the `Hello` class in `my_package.module` by declaring an empty `__all__`, so maybe the module should be made private instead: `my_package/_module.py`. See other hiding techniques in the [Module layout](#module-layout) section.
351
351
352
-
GRIFFE: **Our recommendation — Expose public objects in single locations, use meaningful names.**
352
+
GRIFFE: **Our recommendation — Expose public objects in single locations, use meaningful names.**
353
353
We recommend making sure that each public object is exposed in a single location. Ensuring unique names might be more tricky depending on the code base, so we recommend ensuring meaningful names at least, not requiring the context of modules above to understand what the objects are for.
354
354
355
355
## Logger names
@@ -372,14 +372,14 @@ Could we emit a deprecation warning when users obtain the logger with the old na
372
372
373
373
Since it is essentially impossible to deprecate a logger name, we recommend to avoid using `__name__` as logger name, at the very least in private modules.
374
374
375
-
GRIFFE: **Our recommendation — Use a single logger.**
375
+
GRIFFE: **Our recommendation — Use a single logger.**
376
376
Absolutely avoid using `__name__` as logger name in private modules. If your module layout is hidden, or does not matter for logging purposes, just use the same logger everywhere by using your package name as logger name. Example: `logger = logging.getLogger("griffe")`. Show your users how to temporarily alter your global logger (typically with context managers) so that altering subloggers becomes unnecessary. Maybe even provide the utilities to do that.
377
377
378
378
## Documentation
379
379
380
380
Obviously, your public API should be documented. Each object should have a docstring that explains why the object is useful and how it is used. More on that in our [docstrings recommendations](docstrings.md). Docstrings work well for offline documentation; we recommend exposing your public API online too, for example with [MkDocs](https://www.mkdocs.org/) and [mkdocstrings' Python handler](https://mkdocstrings.github.io/python/), or with other SSGs (Static Site Generators). Prefer a tool that is able to create a [Sphinx-like](https://sphobjinv.readthedocs.io/en/stable/syntax.html) inventory of objects (an `objects.inv` file) that will allow other projects to easily cross-reference your API from their own documentation. Make sure each and every object of your public API is documented in your web docs and therefore added to the objects inventory (and maybe that nothing else is added to this inventory as "public API").
381
381
382
-
> GRIFFE: **Our recommendation — Document your public API extensively.**
382
+
> GRIFFE: **Our recommendation — Document your public API extensively.**
383
383
> Write docstrings for each and every object of your public API. Deploy online documentation where each object is documented and added to an object inventory that can be consumed by third-party projects. If you find yourself reluctant to document a public object, it means that this object should maybe be internal instead.
384
384
>
385
385
> Our documentation framework of choice is of course [MkDocs](https://www.mkdocs.org) combined with our [mkdocstrings](https://mkdocstrings.github.io/) plugin.
@@ -396,14 +396,14 @@ We invite you to check out our own test file: [`test_api.py`](https://github.com
396
396
- all public objects are added to the inventory (which means they are documented in our API docs)
397
397
- no private object is added to the inventory
398
398
399
-
GRIFFE: **Our recommendation — Test your API declaration early.**
399
+
GRIFFE: **Our recommendation — Test your API declaration early.**
400
400
The sooner you test your API declaration, the better your code base will evolve. This will force you to really think about how your API is exposed to your users. This will prevent mistakes like leaving a new object as public while you don't want users to start relying on it, or forgetting to expose a public object in your top-level module or to document it in your API docs.
401
401
402
402
## Linters
403
403
404
404
Depending on their configuration, many popular Python linters will warn you that you access or import private objects. This doesn't play well with hidden module layouts, where modules are private or moved under a private (sub-)package. Sometimes it doesn't even play well with private methods
405
405
406
-
> GRIFFE: **Our recommendation — Ignore "protected access" warnings for your own package, or make the warnings smarter.**
406
+
> GRIFFE: **Our recommendation — Ignore "protected access" warnings for your own package, or make the warnings smarter.**
407
407
> To users of linters, we recommend adding `# noqa` comments on the relevant code lines, or globally disabling warnings related to "private object access" if per-line exclusion requires too much maintenance.
408
408
>
409
409
> To authors of linters, we recommend (if possible) making these warnings smarter: they shouldn't be triggered when private objects are accessed from within the *same package*. Marking objects as private is meant to prevent downstream code from using them, not to prevent the developers of the current package themselves from using them: they know what they are doing and should be allowed to use their own private objects without warnings. At the same time, they don't want to disable these warnings *globally*, so the warnings should be derived in multiple versions, or made smarter.
@@ -497,7 +497,7 @@ Now instead of having to call `main(["dump", "..."])` in your tests, you can dir
497
497
498
498
The third and next users of your CLI as API are your users: just as you made your own life easier, you made their life easier for when they want to call some subcommands of your tool programmatically. No more messing with lists of strings without autocompletion or linting, no more patching of `sys.argv`, no more following the maze of transformations applied by this fancy CLI framework before finally reaching the crux of the subcommand you want to call, no more trying to replicate these transformations yourself with the CLI framework's API to avoid copy-pasting the dozens of lines you're only interested in.
499
499
500
-
> GRIFFE: **Our recommendation — Decouple command-line parsing from your CLI entrypoints.**
500
+
> GRIFFE: **Our recommendation — Decouple command-line parsing from your CLI entrypoints.**
501
501
> Do not tie the command parsing logic with your program's logic. Create functions early, make them accept arguments using basic types (`int`, `str`, `list`, etc.) so that your users can call your main command or subcommands with a single import and single statement. Do not encode all the logic in a single big `main` function. Decoupling the CLI-parsing logic from your entrypoints will make them much easier to test and use programmatically. Consider your entrypoints part of your API!
502
502
>
503
503
> Our CLI framework of choice is [Cappa](https://pypi.org/project/cappa/).
@@ -510,7 +510,7 @@ There are many different ways of deprecating previous usage of code, which depen
510
510
511
511
In addition to emitting deprecation warnings, you should also update the docstrings and documentation for the old usage to point at the new usage, add "deprecated" labels where possible, and mark objects as deprecated when possible.
512
512
513
-
GRIFFE: **Our recommendation — Allow a deprecation period, document deprecations.**
513
+
GRIFFE: **Our recommendation — Allow a deprecation period, document deprecations.**
514
514
Try allowing deprecation periods for every breaking change. Most changes can be made backward-compatible at the cost of writing legacy code. Use tools like [Yore](https://pawamoy.github.io/yore) to manage legacy code, and standard utilities like [`warnings.deprecated`][] to mark objects as deprecated. Griffe extensions such as [griffe-warnings-deprecated](https://mkdocstrings.github.io/griffe-warnings-deprecated/) can help you by dynamically augmenting docstrings for your API documentation.
515
515
516
516
## Third-party libraries
@@ -522,5 +522,3 @@ A few third-party libraries directly or indirectly related to public APIs deserv
522
522
[modul](https://pypi.org/project/modul/), from Frost Ming, the author of [PDM](https://pdm-project.org/en/latest/), goes one step further and actually hides attributes that are not marked "exported" from users: they won't be able to access un-exported attributes, leaving *only* the public API visible.
523
523
524
524
[Deprecated](https://pypi.org/project/Deprecated/), which was probably a source of inspiration for [PEP 702](https://peps.python.org/pep-0702/), allows decorating objects with `@deprecated` to mark them as deprecated. Such decorated callables will emit deprecation warnings when called. PEP 702's `warnings.deprecated` could be seen as its successor, bringing the feature directly into the standard library so that type checkers and other static analysis tools can converge on this way to mark objects as deprecated.
525
-
526
-
[slothy](https://pypi.org/project/slothy/), which is less directly related to public APIs, but useful for the case where you are hiding your modules layout and exposing all your public API from the top-level `__init__` module. Depending on the size of your public API, and the time it takes to import everything (memory initializations, etc.), it might be interesting to make all these imports *lazy*. With a lazily imported public API, users who are only interested in a few objects of your public API won't have to pay the price of importing everything.
0 commit comments