Closed CAM-Gerlach closed 9 months ago
Refactor
__init__.py
to be as minimal as possible and avoid inefficiently importing your entire package (or only do so lazily), as generally recommended.
What would this look like? I'm hesitant to do the other two options because we have tooling that increments __version__
in the const.py
file. Option 1 could break any downstream dependencies that rely on __version__
being in the const.py
module (though this is unlikely to be the case, but still possible). Option 2 is not feasible as we've intentionally avoided defining the package's version in multiple spots to simplify releases.
Refactor init.py to be as minimal as possible and avoid inefficiently importing your entire package (or only do so lazily), as generally recommended.
What would this look like?
It would be by a large margin the most involved of the proposed changes; I almost didn't include it at all, other than to point out that solving the bigger issue of the __init__.py
(eager-)importing most of the rest of the package would also solve this issue. The "simple" approach would involve simply eliminating these imports, but that would of course break any client code relying on these conveniences rather than importing them directly from the relevant submodules. To avoid this, you could retain them but have them lazy-loaded, so the imports are only actually performed if the relevant attributes are accessed, using, for example, the SPEC 0001 approach.
Of course, this as mentioned this is likely a far too involved solution just to solve this problem, as opposed to the much more general one of eager-loading your various submodules in the __init__.py
, so I assume you'll want to focus on the other two proposed solutions.
I'm hesitant to do the other two options because we have tooling that increments version in the const.py file.
Could you not simply replace const.py
with __init__.py
in your custom tools/set_version.py
script? As far as I can tell, it should otherwise work exactly the same.
Option 1 could break any downstream dependencies that rely on version being in the const.py module (though this is unlikely to be the case, but still possible).
As you say seems very unlikely that users would be importing it from praw.const
instead of the more obvious and much more conventional top-level praw
(and there are few good third-party programmatic use cases for __version__
to begin with—I mostly find it useful interactively, e.g. for debugging).
We can actually quantify this, though, using e.g. grep.app which searches all of GitHub, which finds:
Search | praw |
praw.const |
prawcore |
prawcore.const |
---|---|---|---|---|
import X |
485 hits | [0 hits]() | 73 hits | 0 hits |
from X import |
68 hits | 3 hits | 46 hits | 0 hits |
from X import Y |
N/A | 0 hits | N/A | 0 hits |
X.__version__ |
4 hits | 0 hits | 1 hit | 0 hits |
from X import .*__version__ |
2 hits | 0 hits | 0 hits | 0 hits |
So, as far as public code on GitHub goes prawcore.const
isn't even imported once (and even praw.const
is only imported 3 times total, two of them in PRAW itself, none for __version__
), never mind is __version__
used from it, and even the package-level __version__
is only used a couple times. Therefore, it seems fairly safe to conclude that it is rather unlike that even one user is importing it from __version__
, which is substantially fewer users than who may be impacted now and in the future by this missing dependency issue.
Nevertheless, we could resolve even this small chance by importing __version__
in const.py
from the top-level prawcore
package—unless that resulted in an import cycle, which if so is another reason why eager-importing everything in __init__
is generally not usually a great idea.
Option 2 is not feasible as we've intentionally avoided defining the package's version in multiple spots to simplify releases.
That's true, though there are non-trivial tooling and ecosystem benefits to declaring it as static metadata, and I assume you could just tweak your version bump script to update the version there too—though I understand the desire to single-source it, of course.
Overall, Option 1 seems by far the least net disruptive. I could submit a PR for this, if you'd prefer. Thanks!
Makes sense to me.
Overall, Option 1 seems by far the least net disruptive. I could submit a PR for this, if you'd prefer. Thanks!
If you'd like, please do!
Thanks! Opened as PR #165
Thanks for circling back and closing this out! I opened praw-dev/praw#2004 to cover this in PRAW itself (and we in turn might want to reconsider Approach 2 here, if that's what's desired over there and in asyncpraw
, etc).
Describe the Bug
Due to the use of Flit's fallback automatic metadata version extraction needing to dynamically import the package
__init__.py
instead of reading it statically (see pypa/flit#386 ) , since the actual__version__
is imported fromconst.py
, this requiresrequests
be installed in the build environment when building or installing from an sdist (or the source tree).This happens merely by chance to currently be a transitive backend dependency of
flit_core
and thus building in isolated mode works. However, this shouldn't be relied upon, as if eitherflit_core
orprawcore
's dependencies were to ever change, this would break isolated builds too. Andrequests
isn't an exposed non-backend dependency offlit-core
, so it doesn't actually get installed otherwise.This means Requests must be manually installed (not just the explicitly pyproject,toml-specified build dependencies) if building/installing
prawcore
in any other context thanpip
's isolated mode, e.g. without the--no-build-isolation
flag, via the legacy non-PEP 517 builder, or via other tools or most downstream ecosystems that use other build isolation mechanisms. In particular, this was an issue on conda-forge/prawcore-feedstock#14 where I was packaging the newprawcore
2.4.0 version for Conda-Forge—you can see the full Azure build log.Of course, this can be worked around for now by manually installing
requests
into the build environment, but that's certainly not an ideal solution as it adds an extra build dependency (and its dependencies in turn), requires extra work by everyone installing from source (without build isolation), makes builds take longer, and is fragile and not easily maintainable/scalable long term for other runtime dependencies.Desired Result
Prawcore is able to be built and installed without installing requests, or relying on it happening to be a transitive build backend dependency of
flit_core
and present "by accident".There are several ways to achieve this, all potentially reasonable options:
__version__
to be in__init__.py
directly, so Flit can find it by static inspection without triggering an importversion
as static metadata in thepyproject.toml
instead of relying on dynamic backend-specific detection__init__.py
to be as minimal as possible and avoid inefficiently importing your entire package (or only do so lazily), as generally recommended.Code to reproduce the bug
The
Reddit()
initialization in my code example does not include the following parameters to prevent credential leakage:client_secret
,password
, orrefresh_token
.Relevant Logs
This code has previously worked as intended.
Yes
Operating System/Environment
Any, tested Linux, Windows
Python Version
Any; tested 3.10, 3.12
prawcore Version
2.4.0 (newely introduced due to switch to Flit)
Anything else?
Just for reference, this was the build environment on Azure:
And this was the relevant portion of the build log: