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

Improve type checking: return ‘Reader’ from open_database() #88

Merged
merged 3 commits into from
Sep 24, 2021

Conversation

wbolster
Copy link
Contributor

This exposes the ‘Reader’ class under its own name (instead of an alias)
and uses it as the return type for open_database(). In case the C
extension is used, cast that to the same ‘Reader’ type for type checking
purposes, since they share an identical API.

The benefits are that this avoids an unnecessary Union return type
(see e.g. python/mypy#1693) and doesn't
require internal types to be exposed with confusing names for type
checking purposes only.

The ‘Reader’ compat helper is unnecessary and can be dropped: the Python
Reader is always available and API compatible.

While at it, simplify and isort the imports.

Mypy reports attr-defined errors (via no_implicit_reexport) because the
‘maxminddb’ module uses implicit re-exports. Adding __all__ makes the
exported API surface explicit, which avoids the type checking errors.
@wbolster
Copy link
Contributor Author

wbolster commented Sep 20, 2021

this is a follow-up to #87 to further improve the type checking story for this project

@wbolster
Copy link
Contributor Author

(gentle ping @oschwald since you looked into this before)

oschwald
oschwald previously approved these changes Sep 20, 2021
Copy link
Member

@oschwald oschwald left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! I don't love the cast, but this seems like an improvement and I don't see it being an issue.

@wbolster
Copy link
Contributor Author

yes i don't like it either, to be clear.

see my comment added in the code. it seemed nontrivial to introduce a base class for use both in python and in the c extension, and have stubs for that, so i took the easy way out. baby steps, etc

@wbolster
Copy link
Contributor Author

oh, an alternative approach would be to fully hide the c extension's Reader inside the Python Reader, making its existence fully an implementation detail, from a public API perspective.

but that's a change i didn't want to make undiscussed (and i expect it would cascade into test suite refactorings)

@oschwald
Copy link
Member

If I am understanding the proposal, I suspect hiding the extension in that way would also have runtime performance costs.

It looks like pylint isn't happy.

@wbolster
Copy link
Contributor Author

yeah, it would be an indirection. (unless you count very ugly dynamically reassignment of methods as an option). hence definitely not something to blindly turn into a pr without thinking and discussion.

typing from phone, will look into pylint later

This exposes the ‘Reader’ class under its own name (instead of an alias)
and uses it as the return type for open_database(). In case the C
extension is used, cast that to the same ‘Reader’ type for type checking
purposes, since they share an identical API.

The benefits are that this avoids an unnecessary Union return type
(see e.g. python/mypy#1693) and doesn't
require internal types to be exposed with confusing names for type
checking purposes only.

The ‘Reader’ compat helper is unnecessary and can be dropped: the Python
Reader is always available and API compatible.

While at it, simplify and isort the imports.
@wbolster
Copy link
Contributor Author

local pylint reported different things, and apparently github actions don't auto-trigger for me. i've added some changes but can't see the result.

@oschwald
Copy link
Member

Related to the pylint error, the module cannot be loaded if the extension is not available:

Traceback (most recent call last):
  File "/home/greg/MaxMind/MaxMind-DB-Reader-python/.eggs/nose-1.3.7-py3.9.egg/nose/failure.py", line 39, in runTest
    raise self.exc_val.with_traceback(self.tb)
  File "/home/greg/MaxMind/MaxMind-DB-Reader-python/.eggs/nose-1.3.7-py3.9.egg/nose/loader.py", line 417, in loadTestsFromName
    module = self.importer.importFromPath(
  File "/home/greg/MaxMind/MaxMind-DB-Reader-python/.eggs/nose-1.3.7-py3.9.egg/nose/importer.py", line 47, in importFromPath
    return self.importFromDir(dir_path, fqname)
  File "/home/greg/MaxMind/MaxMind-DB-Reader-python/.eggs/nose-1.3.7-py3.9.egg/nose/importer.py", line 94, in importFromDir
    mod = load_module(part_fqname, fh, filename, desc)
  File "/home/greg/.pyenv/versions/3.9.0/lib/python3.9/imp.py", line 244, in load_module
    return load_package(name, filename)
  File "/home/greg/.pyenv/versions/3.9.0/lib/python3.9/imp.py", line 216, in load_package
    return _load(spec)
  File "<frozen importlib._bootstrap>", line 711, in _load
  File "<frozen importlib._bootstrap>", line 680, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 790, in exec_module
  File "<frozen importlib._bootstrap>", line 228, in _call_with_frames_removed
  File "/home/greg/MaxMind/MaxMind-DB-Reader-python/maxminddb/__init__.py", line 8, in <module>
    maxminddb.extension = None  # type: ignore
NameError: name 'maxminddb' is not defined

I think this happened due to the removal of import maxminddb.reader before the extension import.

@wbolster
Copy link
Contributor Author

relying on the implicit side effect of an unused import is not ideal 🙃

see the description of the commit i just pushed: 544379f

oschwald
oschwald previously approved these changes Sep 22, 2021
@oschwald
Copy link
Member

pylint still isn't happy. Maybe line 17 could be changed to:

    import maxminddb.extension as _extension

@wbolster
Copy link
Contributor Author

typing from phone, but i think "from . import submodule" is fine? does it actually not work? 🤔

(your suggestion is invalid python i think?)

@wbolster
Copy link
Contributor Author

pylint-dev/pylint#3933

pylint has various bugs in this area it seems

@wbolster
Copy link
Contributor Author

pylint-dev/pylint#3624

another one

@oschwald
Copy link
Member

I believe the suggestion is valid Python. It is the standard way to import a module under another name.

Your version does work. I don't have a strong opinion if you prefer to disable the linter here, although I have to admit that I am not sure I have seen a module imported that way elsewhere.

@wbolster
Copy link
Contributor Author

it's really rather common to from . import submodule, see e.g. this billion+ times downloaded piece of code: https://github.com/python-attrs/attrs/blob/main/src/attr/__init__.py#L7

@oschwald
Copy link
Member

As I mentioned, I am fine with disabling the pylint check for that line. I don't have a strong preference.

If the extension module cannot be imported, the fallback path
would trigger ‘NameError: name 'maxminddb' is not defined’.

While at it:

- Use an underscored name for the optional extension import to hide it
  a bit from view (e.g. autocomplete in interactive Python shells)

- Use relative imports not unnecessarily repeat the module name
  everywhere
@wbolster
Copy link
Contributor Author

i tried to reproduce the pylint error locally, and at first i could not.

it turns out pylint only complains if it is executed against code that does not have the extension. the github action does not install the module before running pylint. if the extension module actually exists, the error does not appear.

locally i have a pip install --editable ., which builds the extension in-place. only after i deleted the .so file, the error appeared.

anyway, the simplest is to just add the ignore so that it works in all scenarios. i've amended the change to do this.

Copy link
Member

@oschwald oschwald left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@oschwald oschwald merged commit 01cac90 into maxmind:main Sep 24, 2021
@wbolster wbolster deleted the single-reader-type-checking branch September 25, 2021 14:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

2 participants