An analysis of module names inside top PyPI packages
joshcannon.me
joshcannon.me
https://pypi.org/project/xml-from-seq/ → xml_from_seq
https://pypi.org/project/cast-from-env/ → cast_from_env
Simple normalization, right? But `pip` installs one with underscores and one with dashes:
>>> from importlib.metadata import metadata
>>> metadata('xml_from_seq')['Name']
'xml_from_seq'
>>> metadata('cast_from_env')['Name']
'cast-from-env'
so that's what ends up in `pip freeze`.I _think_ it's because there a bdist in PyPI for one, and not the other, so `pip` is using different "backends" that normalize the names into `METADATA` differently... ugh.
That isn't why: it's because `cast-from-env`'s sdist is from March 2023, while PEP 625 (which strongly stipulates package name normalization) was adopted in setuptools a year later[1].
But to take a step back: why does the difference in `pip freeze` affect you? It shouldn't matter to `pip`, since PyPI will happily serve from both the normalized and unnormalized names.
It's a shame that there isn't (currently) a reliable way to perform this backwards link: the closest current things are `{dist}.dist-info/METADATA` (unreliable, entirely user controlled) and `direct_url.json` for URL-installed packages, which isn't present for packages resolved from indices.
Edit: PEP 710[1] would accomplish the above, but it's still in draft.
Python gets the "worst" of it in the sense that it's big and has a large diversity of packages, but it's a general consequence of having a packaging ecosystem that's distinct from a given language's import machinery.
When viewing source code without a code editor, many modern languages have no way to know what comes from where. I don't understand why this seems to be the standard for new languages like Rust.
The whole model of naming of apt install <thing> vs port install <thing> is a wargame all of it's own.
Your general point is well made: how you get a distribution, and unpack and install it is quite distinct from how it names inside the language/system namespace it installs into.
Even at the level of ssh vs sshd, there can be confusion. the daemon is configured from sshd_ files, but they live inside /etc/ssh alongside /etc/ssh/ssh_ files configuring the client side.
yaml -> pip install pyyaml
cv2 -> pip install opencv-contrib-python
PIL -> pip install pillow (wtf, this should be a misdemeanor punishable by being forced to used windows for a year)
And can we please ban "py" and "python" from appearing inside the name of python packages?Or else I'm going to start writing some python packages with ".js" in their name.
I don't think that is a thing that happens in real life.
* Practically, one package is associated with exactly one github repository, sometimes a few. You would see implementation switching from JavaScript to TypeScript, but almost never from python to Go. Normally people start a brand new project for that kind of thing. * The reality is that each language has its own library ecosystem, and people reinvent the wheel at least once for each language. I wish we live in a world where you could save the effort, instead implement everything only once and it runs efficiently and has idiomatic APIs everywhere. But that's not how it works. If you create a package for a language, that's it. You could reimplement the same thing like by line in another language, but that would be a different package for that language.
That's the neat part, it's not! You can distribute basically any kind of data with pip, within reason. Iirc Cmake can be pip-installed.
I guess though that there could be an issue with some dependencies being written against PIL and others being written against pillow?
pillow is imported as `PIL` because it's a fork of the original PIL[1]. There's a very strong argument that Python's ability to retain the same import name across package name changes like that is a valuable source of flexibility that has benefited the ecosystem as a whole.
As in, `import pillow as PIL`?
As in, not changing your imports at all, and just changing your dependency from PIL to pillow. This has two substantial advantages:
1. You only have to change one line (the dependency), not an indefinite number of source files. This is less of an issue now that the Python community has high-quality refactoring tools, but it's still the past of least resistance.
2. More importantly: `import pillow as PIL` is not referentially transparent: the `PIL` binding that it introduces is a `module` object, but that object can't be used in subsequent imports. In other words, blindly performing an `import X as Y` refactor would break code like this:
import PIL
from PIL import whatever
You can observe this for yourself locally: >>> import ssl as lol
>>> from lol import CERT_NONE
ModuleNotFoundError: No module named 'lol'
>>> from ssl import CERT_NONE
This is arguably a defect in Python's import and module machinery, but that's how it currently is. Renaming the dependency and keeping the module name is far less fraught. import PIL
does not make PIL.Image available. What the hell else do you expect me to do with PIL? Why isn't PIL.Image included in importing PIL? You have to explicitly do either of these import PIL.Image
from PIL import Image(Understanding the difference between packages, module hierarchies, and module attributes is table stakes for architecting a large Python package correctly. PIL almost certainly does this to prevent hard-to-debug circular imports elsewhere in their codebase.)
I wish it was clearer sometimes what was a module, and what was an attribute in the core import syntax. `import foo; foo.bar` only breaks if it's a module, and `import foo.bar` only breaks if it's an attribute. If you do `from foo import bar`, the syntax works with both.
If you see `os.path.basename` what could `os.path` be? It would be a module most of the time because it's written with lowercase. `itertools.chain.from_iterable` [1] would be a notable exception.
[1]: https://docs.python.org/3/library/itertools.html#itertools.c...
Imports can be aliased, so why allow this mismatch at all? PyPI should have enforced that each package contains one top-level module whose name is identical to the name used to install it.
Now there's a somewhat useful "make a pull request to an open source project" exercise.
I think that's just a misconfiguration due to the relatively common layout of
- .git
- pyproject.toml / setup.py / setup.cfg etc.
- src/mypackage
- tests/test_module1.py
- tests/test_module2.py
Depending on how you configure stuff you might accidentally include the tests directory as a separate top-level package next to all packages under "src". If you stick to the legacy ways, this does not happen if you just used the usual setup(
...,
packages=find_packages('src'),
package_dir={'': 'src'},
)
I think this is the default behavior of setuptools nowadays if you do not say anything at all in any of the config files about where your code is.If you actually intend to ship the tests, because they don't require a specialized environment to run, then the project layout should really be
- .git
- pyproject.toml / setup.py / setup.cfg etc.
- src/mypackage
- src/mypackage/tests/test_module1.py
Downstream consumers who might want to ship this as part of something larger should ideally be able to just delete mypackage/tests without anything breaking.The practice of having tests inside the package being tested I remember as being discouraged, because it makes it hard to run one version of tests against another of the package. Which I guess can be useful for regression testing, though I have not really used it. An alternative layout that would preserve that be a mypackage_tests top-level.
user/package-name group/package-name
etc...
Maybe I invent a protocol today called "hitta" and make a new package called
"hitta"
I'm pretty much automatically going to be the de facto standard, even if better, more updated implementations exist. Names matter.
But if my implementation is called
hittaorg/hitta
Organizations and users (publishers) can be verified, and the tools integrate correctly; you gain better package context, increase trust, and reduce supply chain risks.
Now, if user123 has a better version, they might make
user123/hitta
Instead of
pyhitta-with-new-features
or whatever garbage is used today
And I don't understand what's preventing users and organization from being verified now?
On the other hand, you know, it's already source code, it can do whatever it wants...