Repository navigation
Description
Poetry currently will look up packages in every source, even when unnecessary due to a source = constraint on a dependency, or if the dependency was already found. This behavior often surprises users (though it was originally modeled on pip). Ostensibly, secondary is the solution to this -- however, it merely pushes the repository further back in the search order and does little to address the user complaints/confusion. As such, we should deprecate it and replace it with new options that better match user expectations.
I think, to preserve backwards compatibility, the long-term path forward is probably two different options (in my example, supplemental and private). I would propose something like the following, as an overview of behaviors:
- No options means that the repository overlays PyPI, but we will still fall back to PyPI if necessary (e.g. suitable for internal mirrors),
pip --extra-index-url-style. This is known internally as primary, and is the current behavior. default = truemeans that the repository replaces PyPI,pip --index-url-style. This is the current behavior.supplemental = truemeans that this source is only consulted after a lookup in thedefault(implicitly PyPI unless configured otherwise) and primary sources fail.private = truemeans that the repo will only be considered for packages that are explicitly configured withsource =, and should be preferred by users for private packages to avoid dependency confusion attacks.
secondary = true would be kept around as a deprecated option (likely with a warning), and would maintain the legacy behavior of being searched exhaustively for backwards compatibility.
Originally #5984 (comment)
Activity
neersighted commentedon Oct 5, 2022
#6669 represents incremental progress towards this goal, and the author is interested in implementing this proposal.
MasterNayru commentedon Oct 5, 2022
Would it be fair say that a contributing factor to issues that users have when managing multiple repositories is that Poetry assumes the worst case scenario when it comes to how to obtain metadata from non-PyPI repositories? Lookups to PyPI are done through APIs and happen way quicker than the HTML parsing that happens with the other repositories. Obviously, Poetry has gone down that route to ensure that things work and don't just explode because some backend didn't whatever APIs PyPI is currently exposing.
Just seems to me that while querying repos unnecessarily is worth avoiding, a real pain point is how these other package repositories end up being utilised.
neersighted commentedon Oct 5, 2022
I did not reference it here as it is out of scope for this issue; however, yes, that is a problem. However, it is an ecosystem problem:
(from #4113 (comment))
In short, PyPI implements a non-standard API that has had to make breaking changes with no warning in the past, and that they intend to remove. It has served Poetry's use case so far and validated the need for this in the ecosystem, but it is not implementable by other package sources for a variety of reasons (ranging from the data model being coupled to warehouse internals and incorrect in the general case, to detection being problematic).
There is a standards-based solution and path forward; Poetry will have to wait patiently with everyone else for gradual adoption.
b-kamphorst commentedon Oct 5, 2022
Indeed I am. The refactor is "hairy" (not my words), but it appears to be a nice way to get acquainted with the Poetry code and contribute.
I'm now at the point where I write tests that express the desired behavior and found that the
privatecase needs to be made more explicit. Specifically: if a package is configured withsourceand targets aprivaterepository, what does that mean for finding that package's dependencies?Currently, such dependencies are not bound to the source, meaning that they may be retrieved from any configured repo including the source repo. Perhaps the most natural option is to, when we introduce
privaterepos and a package's source is set to that private repo, its dependencies can be retrieved from all repositories including the private repo that is the package source (and no other private repos). However, there are alternative scenarios as well. For example, only might suggest that dependencies may not consider anyprivaterepo.The first suggestion seems most appropriate. My private packages have private dependencies, so I'd expect that to work just fine. Still, I'd like to hear your thoughts before I make assumptions.
neersighted commentedon Oct 6, 2022
Regarding the correct model for transient dependencies when requested from a
source =withprivate = true, I am personally inclined to take "the easy way out" and require the user to list them as top-level deps withprivate-transient-dep = "*"as the suggested mechanism -- that way constraints are driven by their sibling dependencies.I find this less surprising as all dependencies in Python are "peer dependencies" in other ecosystems, and thus we don't have to answer the question of where a dep comes from when it's a mutual dependency of multiple top level deps with disparate
source =markers.However, if we must propagate
source =, I agree that recursively allowing lookup in aprivate = truesource (of course additive, to solve the mutual tree issue) might be less surprising in the common case. It's just that the design is harder to reason about, harder to document, may interact in surprising ways with non-Poetry tools/expectations, and may complicate things in the complex cases.Poetry has generally taken a stance of "if you care about any details of your transient dependencies, well, now it's a top-level dependency," which is maybe constructed backwards vs. the reality of the Python ecosystem (most existing tooling and standards are built around flat lists of deps and not trees). In my mind, it makes more sense to users who are new to Python packaging and better expresses Poetry's goals of bringing useful ideas from other ecosystems to Python, while remaining compatible/a good citizen.
dimbleby commentedon Oct 6, 2022
Can I suggest setting out what you think the key use cases for configuring repositories are, and spelling out how those use cases would be met by this proposal?
With any luck that'll validate that this is a great solution. With even more luck, it'll help someone to realise that their case is not well served, or that the model can be simplified without hurting anything.
The cases that come to mind for me are
in my small world I don't know the use case for "internal mirror with fallback to pypi", but perhaps it's a thing people want. (Maybe those with slow or limited internet connections have reasons to prefer local mirrors but are willing to fall back to pypi?)
kkozmic commentedon Oct 10, 2022
One more that is crucial to my team:
source = "our-internal-repo". We would expect internal packages to only be looked up in our internal repo, and all other packages to only be looked up in pypistrangemonad commentedon Oct 18, 2022
@kkozmic that is also our primary use case. Specifically, poetry is starting to get unbearably slow in our larger projects. Our private repositories are hosted in gitlab and when gitlab doesn't find a package it returns with a redirect to pypi. This means any local caching is defeated because the requested and returned urls differ
b-kamphorst commentedon Oct 24, 2022
I've made a draft PR to facilitate the discussion. High-level feedback is very welcome (may save future revisions)!
b-kamphorst commentedon Oct 30, 2022
We regularly use an internal mirror with fallback to PyPI. Our packages are developed internally and made public with some delay (and not all versions are released externally). During development of package A, I need to be able to use the latest features of package B -- possibly not available on PyPI yet. When the new versions packages A and B are released externally, users of package A should just use the version of package B that is available on PyPI.
b-kamphorst commentedon Oct 30, 2022
@neersighted is
supplementalbehaviour actually breaking forsecondary? Ifsecondarysources are considered after all other (primary, default) sources, then the resolution behaviour should be identical (apart from making / timing of queries). If not, what am I missing?jgentil commentedon Nov 11, 2022
This is an excellent proposal. In particular, the
privateoption will mitigate so many headaches we have currently, where updating and fresh installs are a total pain because of having to use Azure Devops' PyPI implementation that is terribly slow.vmgustavo commentedon Dec 14, 2022
Is there any palliative solution?
2 remaining items
radoering commentedon Feb 18, 2023
In the last maintainer meeting we revisited the proposal, refined it and came up with a slightly differnt design alternative. I'll try to summarize it here, so we can decide if the alternative or the (revised) original proposal makes more sense.
General decisions (same in both proposals)
Secondary sources (
secondary = true) will be deprecated. The behavior will be kept as is until removal: Secondary sources are queried after the default PyPI source if there are other (non-secondary, i.e. "primary") sources defined. If there are only secondary sources they are queried before the default PyPI source. Secondary sources are always queried if the source is not explicitly specified for a dependency (even if a suitable package/version has already been found in other sources).Setting a default source (
default = true) will be deprecated since it's not required as a priority but only to disable the default PyPI source. For that purpose an explicit option will be introduced (see #7430 for details). The behavior of the deprecated default will be kept as is until removal: Defining a default disables the default PyPI source and the source marked as default becomes the source with the highest priority.Without
secondary = trueanddefault = trueonly one of the existing source types remains. These unlabeled sources (internally called "primary") are queried before the default PyPI source and each primary source is always queried (even if a suitable package/version has already been found in another source).With only one remaining source type we need new options to define the following behaviors:
In the following I'll summarize the revised original proposal and the new variant omitting the deprecated features for more clarity.
Original proposal
Introduce a new property for sources called
priority(ortype) that can have the valuesprimary(same as if not set, behavior as described before),supplementalorexplicit.Supplemental sources will only be queried if no suitable package/version has been found in other (primary and supplemental) sources before. The order of supplemental sources is the order of occurrence as in pyproject.toml.
Explicit sources are only queried if they are defined explicitly for a dependency.
New proposal
Introduce two new flags:
serialandexplicit. Explicit is the same as before. The priority/order of sources is only defined by the order in the pyproject.toml. Unflagged (aka "primary") sources are (kind of) queried in parallel (not really but each source is queried even if a package has been found in another one). If a source is labelled as serial, it is only queried if no suitable package/version has been found before.In contrast to the original proposal, sources are not sorted by groups first, but divided into groups by the order of occurrence in the pyproject.toml. Let's consider the following example:
That would result in A and B being queried first "in parallel". If no suitable package/version is found in A or B, C and D are queried "in parallel". If no suitable package/version is found in C or D, E is queried. (If a package/version is found in one group the following groups are not queried.)
Closing thoughts
In both proposals, the default PyPI source (if not disabled) has the lowest priority and is only queried if no suitable package/version has been found in any other (non-explicit) source. This should help to avoid dependency confusion attacks.
What do you think? Which proposal makes more sense? Which one is easier to teach? (The names of the new options are not carved in stone yet and may be replaced if there are more suitable names.) Is there any use case not covered by one (or even both) proposals?
b-kamphorst commentedon Feb 20, 2023
Thank you for the detailed overview and proposal. Perhaps the one thing that can be clarified is what the resolution order implies (perhaps also to add to the docs in the PR that handles the final proposal)?
To the best of my understanding (please agree / correct). If several sources are queried for a certain dependency in the same 'batch' (that includes the current
"primary",defaultandsecondarywhich are all queried before their respective packages are inspected), then all package versions for the dependency from all queried sources are returned bypoetry.repositories.repository_pool.RepositoryPool.find_packages. There are only two callers of this function, namelypoetry.puzzle.provider.Provider.search_forandpoetry.version.version_selector.VersionSelector.find_best_candidate. In both cases, the packages are sorted in order of decreasing package version. As a result poetry always tries to satisfy all constraints with the package that has the highest version and this package is retrieved from the first source that yielded it. So if asecondarysource yields version1.0.1and all other (primary, default) sources yield version1.0.0, then the secondary source takes the win. If another source that provides the same version is queried earlier in the batch, then that source takes the win. A next batch (new behaviour) is considered only if none of the sources in the previous batch has a package that meets the current constraints.As for the general decisions part: you have my full support in both decisions.
Regarding the other part, my thoughts are the following. The new proposal is strictly more generic than the original proposal -- if every supplemental source is flagged as
serialthan we obtain the first approach. However, it also seems to be harder to teach and more error-prone on the user side. The behavior needs manual adjustment ofpyproject.tomlas the implicit behavior cannot be provided through CLI. If I as a user expected a package to be retrieved from source D but it is retrieved from source A, the implicit behavior makes it harder for me to understand how the flag in source C is significant in the process. Suitable debug logging can help but we can not actively help the user to get this right. As such, I lean towards the first proposal UNLESS there is a clear use case that requires the second proposal.Looking forward to others' thoughts.
ralbertazzi commentedon Mar 2, 2023
Hi agree with @b-kamphorst: the first solution seems easier to understand and - while less generic - it also feels less "over-engineered". Did you ever receive issues that the first proposal would not solve but the second one would?
ralbertazzi commentedon Mar 2, 2023
For anyone having the same issue of @kkozmic , I developed a simple opinionated plugin that makes sure that:
sourcespecifier are only fetched from that source (Poetry already ensures that)sourcespecifier that have a result from the default source (pypi) are not searched in other repositoriesYou can find it here, feedback is welcome!
https://github.com/ralbertazzi/poetry-private-repo-plugin
radoering commentedon Mar 8, 2023
While writing down the two approaches, I already had the feeling that the new proposal was too elaborate. Thanks for the confirmation. I think we will stick (more or less) to the original proposal.
#6713 (comment) shows that there is something we did not yet consider: People might have different requirements considering the priority of the default PyPI source. Some users want other sources only be looked up if there is nothing on PyPI, other users want PyPI only be looked up if there is nothing in other sources. Thus, it might make sense to make the priority of PyPI configurable.
a-recknagel commentedon Mar 8, 2023
Just adding one more voice to the pool of "having a
privateflag on a source would be great".Works for me, because if things get complicated I'd probably just set up a "pypi mirror plus private packages" and use that for everything, which solves the issue as a whole. Controlling the lookup and fallback order through clever setting of ever more granular flags for both sources and dependencies seems difficult and error prone.
radoering commentedon Apr 16, 2023
Short status update:
The first step is done: #7658 has been merged into master. Thus, the next minor version of Poetry will introduce explicit package sources, which are only searched if they are explicitly configured for a dependency with
source = ....IMO, the next step is #7801, which will allow to configure the priority of PyPI. In its issue description, I also described my target image. (#7801 will supersede #7430.) Opinions are welcome.
jaklan commentedon Jun 2, 2023
@radoering I have a question about that part:
I don't really understand why this exception is only relevant to the
explicitpriority, but not to thesupplementalone. If I add some source as asupplemental, it means I expect something different to be theprimaryone, which is also stated in docs quite clearly:If I don't specify any
primarysource directly, imho it's quite obvious to assume it should be still PyPI.Also, the scenario in which a) we don't have any
primarysources b) we have thesupplementalones c) PyPI is set asexplicit/supplementalshould be then treated as a wrong configuration and raise an error (e.g.You have to configure at least one primary source).Btw, the current docs are quite confusing:
there is no description what
primarysources are and how they are different from thesupplementalones - especially that information:is really missing, together with a note what happens if a higher version of package is found in a lower-priority
primarysource:if higher package version wins, it means that note:
is not really true
if order of sources wins, that raises a question how to achieve the above behaviour (== look for the highest version in a set of sources)
similar issue with
supplementalsources: if we define multiplesupplementalsources and package is not found in anyprimaryone - are they all queried as well or maybe they are queried in order of appearance inpyproject.toml? What takes precedence: sources order or higher package version?imho we should already mark the
defaultsource asdeprecated- docs in the current state really encourage to usedefault:primarysources are not documented, sodefaultseems as a "promoted" approachit's always mentioned as a first solution to disable PyPI
Both these points don't make too much sense if we want to get rid of
defaultsoon. Instead of that, we should promote disabling PyPI by configuring it asexplicitand defining at least oneprimarysource, as that's going to be the target approach anyway (with an annotation that the first part wouldn't be needed anymore when automatic PyPI disabling is enabled in a future version).radoering commentedon Jun 3, 2023
It has a technical background. We need at least one non-explicit source. Otherwise, we'll get a "no versions for package xyz found" error which would be quite obscure.
Your reasoning makes sense. We probably should change it.
Technically, that's not an issue because if nothing is found in higher priority sources (which would be always true in this case), supplemental sources are searched. However, from a teaching perspective, I agree with you. If we want to change it we should print a warning first to avoid a breaking change (and change it to an error later).
PRs with docs improvements are always welcome. 😄
That's still true. If the same version of a package is found in two sources the higher priority wins.
In that case, at first, only the first supplemental source will be searched. Only if no versions fulfilling the constraint are found in the first one, the second supplemental source will be searched, and so on.
We can't (don't want to) deprecate
defaultbefore we we make this warning become true: "In a future version of Poetry, PyPI will be disabled automatically if there is at least one custom source configured with another priority than explicit."That is an alternative we also considered. However, we decided that it's better to keep
defaultand deprecate it as mentioned above so that users don't have to add an explicit PyPI (that can be removed later) if they don't want to use PyPI at all. With the chosen approach, users only have to change their configuration once (when we deprecate default). With the alternative, users have to change their configuration twice. (Strictly speaking, they don't have to remove the explicitly configured PyPI but IMO it would be weird to keep it.) This would feel like a back and forth to me.jaklan commentedon Jun 3, 2023
@radoering thank you for the quick answer!
Fully agree with that approach. As you said - technically it is correct to have
supplementalsource(s) without anyprimarysources, but that could be really confusing for end-user from a conceptual perspective to trigger PyPI disabling as a side effect then. I believe the assumption ofat least one (either explicit or implicit PyPI) primary sourcewould improve general user experience.To sum up then:
primarysources are queried all together and the highest package version wins; if the same package version is found in multiple sources, the higher-priority source winssupplementalsources are queried one by one, so the first found package version always winsIs that right? If yes, I wonder if that's actually the expected behaviour for users, as that's a bit "inconsistent". I could imagine some people wish to query
primarysources one by one, and vice versa - to query thesupplementalones all together. Maybe we should think about making that behaviour configurable for all types of sources (so also theexplicitones, as that would be useful if we want to support multiple per-package source constraints in the future)? For example:Yeah, I get your point, I also had the same thoughts, just assumed redundant, but not-breaking,
explicitPyPI would be less affecting than deprecateddefault- but I agree it doesn't really matter so much asdefaultwas already used in the past versions, so user action is needed anyway. Imho docs aboutprimarysources and a quick note thatdefaultis not yet deprecated, but it will be very soon, would do a job here and significantly clarify both current and future state of sources.I don't want to overpromise, but when I have the full picture (so when we discuss the above points), maybe I will find some time in the foreseeable future to prepare at least initial PR.
radoering commentedon Jun 3, 2023
In principle, yes. Strictly speaking, it's not always the highest version but the highest version that fulfills all constraints.
In that case, they can configure one source as
primaryand all others assupplementalsince that's the same.We considered making it more elaborate in #6713 (comment) but refrained from doing so since there were no real-world use cases.
Supporting multiple per-package source constraints is a new feature request. If you really have a use case for this, please create a new issue (if there is no such issue yet), so that we can discuss the consequences there.
Makes sense.
jaklan commentedon Jun 3, 2023
@radoering
Sure thing.
Right, good point.
Yep, I agree querying
primarysources one by one is more important use-case, but as mentioned above - it's actually achievable, so then we can simply observe if there's a real need for querying thesupplementalones all together and don't take any actions as for now.That was actually just an example why we could make sources "priority" configurable for all types, not only
primaryandsupplemental😉 But if we talk about that functionality per se - personally I don't have such a need, however - I could imagine a scenario when it's useful (e.g. you want to add PyPI or another source as the "backup" one, but only for specific package(s), not all).github-actions commentedon Feb 29, 2024
This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.