Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add types #282

Merged
merged 23 commits into from
Nov 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ Backward-incompatible changes:
The package meta data should ensure that you keep getting 20.1.0 on those versions.
`#244 <https://github.com/hynek/structlog/pull/244>`_

- ``structlog`` is now fully type-annotated.
This won't break your applications but if you use mypy, it will most likely break your CI.

Check out the new chapter on typing for details.


Deprecations:
^^^^^^^^^^^^^
Expand All @@ -35,6 +40,13 @@ Changes:
- Added ``structlog.BytesLogger`` to avoid unnecessary encoding round trips.
Concretely this is useful with *orjson* which returns bytes.
`#271 <https://github.com/hynek/structlog/issues/271>`_
- ``structlog`` has now type hints for all of its APIs!
Since ``structlog`` is highly dynamic and configurable, this led to a few concessions like a specialized ``structlog.stdlib.get_logger()`` whose only difference to ``structlog.get_logger()`` is that it has the correct type hints.

We consider them provisional for the time being – i.e. the backward compatibility does not apply to them in its full strength until we feel we got it right.
Please feel free to provide feedback!
`#223 <https://github.com/hynek/structlog/issues/223>`_,
`#282 <https://github.com/hynek/structlog/issues/282>`_


----
Expand Down
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
include LICENSE LICENSE.apache2 LICENSE.mit conftest.py
include *.rst
include src/structlog/py.typed typing_examples.py

include *.ini *.yml *.yaml *.toml
graft .github
Expand Down
27 changes: 26 additions & 1 deletion docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ API Reference

.. autoclass:: ExceptionPrettyPrinter

.. autoclass:: TimeStamper(fmt=None, utc=True, key="timestamp")
.. autoclass:: TimeStamper

.. doctest::

Expand All @@ -213,6 +213,8 @@ API Reference

.. automodule:: structlog.stdlib

.. autofunction:: get_logger

.. autoclass:: BoundLogger
:members: bind, unbind, new, debug, info, warning, warn, error, critical, exception, log

Expand All @@ -235,6 +237,29 @@ API Reference
:members: wrap_for_formatter


`structlog.types` Module
------------------------

.. automodule:: structlog.types

.. autoprotocol:: BindableLogger

Additionally to the methods listed below, bound loggers **must** have a ``__init__`` method with the following signature:

.. method:: __init__(self, wrapped_logger: WrappedLogger, processors: Iterable[Processor], context: Context) -> None
:noindex:

Unfortunately it's impossible to define initializers using `PEP 544 <https://www.python.org/dev/peps/pep-0544/>`_ Protocols.

They currently also have to carry a `Context` as a ``_context`` attribute.

.. autodata:: EventDict
.. autodata:: WrappedLogger
.. autodata:: Processor
.. autodata:: Context
.. autodata:: ExcInfo


`structlog.twisted` Module
--------------------------

Expand Down
28 changes: 7 additions & 21 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,6 @@
# 2.0, and the MIT License. See the LICENSE file in the root of this
# repository for complete details.


# This file is execfile()d with the current directory set to its containing dir
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.

import codecs
import os
import re
Expand All @@ -36,23 +27,15 @@ def find_version(*file_paths):
raise RuntimeError("Unable to find version string.")


# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
# sys.path.insert(0, os.path.abspath('.'))

# -- General configuration ----------------------------------------------------

# If your documentation needs a minimal Sphinx version, state it here.
# needs_sphinx = '1.0'

# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.autodoc.typehints",
"sphinx.ext.doctest",
"sphinx.ext.intersphinx",
"sphinx.ext.viewcode",
"sphinx_toolbox.more_autodoc.autoprotocol",
]

# Add any paths that contain templates here, relative to this directory.
Expand Down Expand Up @@ -100,10 +83,13 @@ def find_version(*file_paths):
default_role = "any"

nitpick_ignore = [
("py:class", "callable"),
("py:class", "file object"),
("py:class", "BinaryIO"),
("py:class", "ILogObserver"),
("py:class", "PlainFileObserver"),
("py:class", "TLLogger"),
("py:class", "TextIO"),
("py:class", "structlog._base.BoundLoggerBase"),
("py:class", "structlog.dev._Styles"),
]

# If true, '()' will be appended to :func: etc. cross-reference text.
Expand Down
2 changes: 1 addition & 1 deletion docs/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ While wrapped loggers are *immutable* by default, this example demonstrates how

Please note that `structlog.stdlib.LoggerFactory` is a totally magic-free class that just deduces the name of the caller's module and does a `logging.getLogger` with it.
It's used by `structlog.get_logger` to rid you of logging boilerplate in application code.
If you prefer to name your standard library loggers explicitly, a positional argument to `get_logger` gets passed to the factory and used as the name.
If you prefer to name your standard library loggers explicitly, a positional argument to `structlog.get_logger` gets passed to the factory and used as the name.


.. _twisted-example:
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Basics
processors
examples
development
types


Integration with Existing Systems
Expand Down
7 changes: 7 additions & 0 deletions docs/standard-library.rst
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,15 @@ It behaves exactly like the generic `structlog.BoundLogger` except:

- it's slightly faster due to less overhead,
- has an explicit API that mirrors the log methods of standard library's `logging.Logger`,
- it has correct type hints,
- hence causing less cryptic error messages if you get method names wrong.

----

If you're using static types (e.g. with mypy) you also may want to use `structlog.stdlib.get_logger()` that has the appropriate type hints if you're using `structlog.stdlib.BoundLogger`.
Please note though, that it will neither configure nor verify your configuration.
hynek marked this conversation as resolved.
Show resolved Hide resolved
It will call `structlog.get_logger()` just like if you would've called it -- the only difference are the type hints.


Processors
----------
Expand Down
43 changes: 43 additions & 0 deletions docs/types.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
Type Hints
==========

Static type hints -- together with a type checker like `Mypy <https://mypy.readthedocs.io/en/stable/>`_ -- are an excellent way to make your code more robust, self-documenting, and maintainable in the long run.
And as of 20.2.0, ``structlog`` comes with type hints for all of its APIs.

Since ``structlog`` is highly configurable and tries to give a clean facade to its users, adding types without breaking compatibility, while remaining useful was a formidable task.

If you used ``structlog`` and Mypy before 20.2.0, you will probably find that Mypy is failing now.
As a quick fix, add the following lines into your ``mypy.ini`` that should be at the root of your project directory (and must start with a ``[mypy]`` section):

.. code:: ini

[mypy-structlog.*]
follow_imports = skip

It will ignore ``structlog``'s type stubs until you're ready to adapt your code base to them.


----

The main problem is that `structlog.get_logger()` returns whatever you've configured the bound logger to be.
The only commonality are the binding methods like ``bind()`` and we've extracted them into the `structlog.types.BindableLogger` :class:`~typing.Protocol`.
But using that as a return type is worse than useless, because you'd have to use `typing.cast` on every logger returned by `structlog.get_logger()`, if you wanted to actually call any logging methods.

The second problem is that said ``bind()`` and its cousins are inherited from a common base class (a `big <https://www.youtube.com/watch?v=3MNVP9-hglc>`_ `mistake <https://python-patterns.guide/gang-of-four/composition-over-inheritance/>`_ in hindsight) and can't know what concrete class subclasses them and therefore what type they are returning.
hynek marked this conversation as resolved.
Show resolved Hide resolved

The chosen solution is adding `structlog.stdlib.get_logger()` that just calls `structlog.get_logger()` but has the correct type hints and adding `structlog.stdlib.BoundLogger.bind` et al that also only delegate to the base class.

`structlog.get_logger()` is typed as returning `typing.Any` so you can use your own type annotation and stick to the old APIs, if that's what you prefer:

.. code::

import structlog

logger: structlog.stdlib.BoundLogger = structlog.get_logger()
logger.info("hi") # <- ok
logger.msg("hi") # <- mypy: 'error: "BoundLogger" has no attribute "msg"'

----

Rather sooner than later, the concept of the base class will be replaced by proper delegation that will put the context-related methods into a proper class (with proxy stubs for backward compatibility).
In the end, we're already delegating anyway.
28 changes: 28 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[mypy]
# show error messages from unrelated files
follow_imports = normal

# suppress errors about unsatisfied imports
ignore_missing_imports = True

# be strict
check_untyped_defs = True
disallow_any_generics = True
disallow_incomplete_defs = True
disallow_untyped_calls = True
disallow_untyped_defs = True
no_implicit_optional = True
strict_optional = True
warn_no_return = True
warn_redundant_casts = True
warn_unreachable = True
warn_unused_ignores = True

# sometimes redefinition is just fine
allow_redefinition = True

[mypy-tests.*]
ignore_errors = True

[mypy-conftest]
ignore_errors = True
10 changes: 10 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ source = ["src", ".tox/*/site-packages"]
[tool.coverage.report]
show_missing = true
skip_covered = true
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:",
"raise NotImplementedError",
# typing-related code
"^if (False|TYPE_CHECKING):",
": \\.\\.\\.$",
"^ +\\.\\.\\.$",
"-> ['\"]?NoReturn['\"]?:",
hynek marked this conversation as resolved.
Show resolved Hide resolved
]


[tool.black]
Expand Down
7 changes: 5 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@
"Topic :: Software Development :: Libraries :: Python Modules",
]
PYTHON_REQUIRES = ">=3.6"
INSTALL_REQUIRES = []
INSTALL_REQUIRES = [
"typing-extensions; python_version<'3.8'",
]
EXTRAS_REQUIRE = {
"tests": [
"coverage[toml]",
Expand All @@ -47,7 +49,7 @@
"pytest>=6.0",
"simplejson",
],
"docs": ["furo", "sphinx", "twisted"],
"docs": ["furo", "sphinx", "sphinx-toolbox", "twisted"],
}
EXTRAS_REQUIRE["dev"] = (
EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["docs"] + ["pre-commit"]
Expand Down Expand Up @@ -129,6 +131,7 @@ def find_meta(meta):
python_requires=PYTHON_REQUIRES,
install_requires=INSTALL_REQUIRES,
extras_require=EXTRAS_REQUIRE,
include_package_data=True,
zip_safe=False,
options={"bdist_wheel": {"universal": "1"}},
)
7 changes: 4 additions & 3 deletions src/structlog/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"""


from structlog import dev, processors, stdlib, testing, threadlocal
from structlog import dev, processors, stdlib, testing, threadlocal, types
from structlog._base import BoundLoggerBase, get_context
from structlog._config import (
configure,
Expand All @@ -33,12 +33,12 @@
try:
from structlog import twisted
except ImportError:
twisted = None
twisted = None # type: ignore

try:
from structlog import contextvars
except ImportError:
contextvars = None
contextvars = None # type: ignore


__version__ = "20.2.0.dev0"
Expand Down Expand Up @@ -79,5 +79,6 @@
"testing",
"threadlocal",
"twisted",
"types",
"wrap_logger",
]
Loading