uv_distribution/metadata/
lowering.rs

1use std::collections::BTreeMap;
2use std::io;
3use std::path::{Path, PathBuf};
4
5use either::Either;
6use thiserror::Error;
7
8use uv_distribution_filename::DistExtension;
9use uv_distribution_types::{
10    Index, IndexLocations, IndexMetadata, IndexName, Origin, Requirement, RequirementSource,
11};
12use uv_git_types::{GitReference, GitUrl, GitUrlParseError};
13use uv_normalize::{ExtraName, GroupName, PackageName};
14use uv_pep440::VersionSpecifiers;
15use uv_pep508::{MarkerTree, VerbatimUrl, VersionOrUrl, looks_like_git_repository};
16use uv_pypi_types::{ConflictItem, ParsedGitUrl, ParsedUrlError, VerbatimParsedUrl};
17use uv_redacted::{DisplaySafeUrl, DisplaySafeUrlError};
18use uv_workspace::Workspace;
19use uv_workspace::pyproject::{PyProjectToml, Source, Sources};
20
21use crate::metadata::GitWorkspaceMember;
22
23#[derive(Debug, Clone)]
24pub struct LoweredRequirement(Requirement);
25
26#[derive(Debug, Clone, Copy)]
27enum RequirementOrigin {
28    /// The `tool.uv.sources` were read from the project.
29    Project,
30    /// The `tool.uv.sources` were read from the workspace root.
31    Workspace,
32}
33
34impl LoweredRequirement {
35    /// Combine `project.dependencies` or `project.optional-dependencies` with `tool.uv.sources`.
36    pub(crate) fn from_requirement<'data>(
37        requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
38        project_name: Option<&'data PackageName>,
39        project_dir: &'data Path,
40        project_sources: &'data BTreeMap<PackageName, Sources>,
41        project_indexes: &'data [Index],
42        extra: Option<&ExtraName>,
43        group: Option<&GroupName>,
44        locations: &'data IndexLocations,
45        workspace: &'data Workspace,
46        git_member: Option<&'data GitWorkspaceMember<'data>>,
47    ) -> impl Iterator<Item = Result<Self, LoweringError>> + use<'data> + 'data {
48        // Identify the source from the `tool.uv.sources` table.
49        let (sources, origin) = if let Some(source) = project_sources.get(&requirement.name) {
50            (Some(source), RequirementOrigin::Project)
51        } else if let Some(source) = workspace.sources().get(&requirement.name) {
52            (Some(source), RequirementOrigin::Workspace)
53        } else {
54            (None, RequirementOrigin::Project)
55        };
56
57        // If the source only applies to a given extra or dependency group, filter it out.
58        let sources = sources.map(|sources| {
59            sources
60                .iter()
61                .filter(|source| {
62                    if let Some(target) = source.extra() {
63                        if extra != Some(target) {
64                            return false;
65                        }
66                    }
67
68                    if let Some(target) = source.group() {
69                        if group != Some(target) {
70                            return false;
71                        }
72                    }
73
74                    true
75                })
76                .cloned()
77                .collect::<Sources>()
78        });
79
80        // If you use a package that's part of the workspace...
81        if workspace.packages().contains_key(&requirement.name) {
82            // And it's not a recursive self-inclusion (extras that activate other extras), e.g.
83            // `framework[machine_learning]` depends on `framework[cuda]`.
84            if project_name.is_none_or(|project_name| *project_name != requirement.name) {
85                // It must be declared as a workspace source.
86                let Some(sources) = sources.as_ref() else {
87                    // No sources were declared for the workspace package.
88                    return Either::Left(std::iter::once(Err(
89                        LoweringError::MissingWorkspaceSource(requirement.name.clone()),
90                    )));
91                };
92
93                for source in sources.iter() {
94                    match source {
95                        Source::Git { .. } => {
96                            return Either::Left(std::iter::once(Err(
97                                LoweringError::NonWorkspaceSource(
98                                    requirement.name.clone(),
99                                    SourceKind::Git,
100                                ),
101                            )));
102                        }
103                        Source::Url { .. } => {
104                            return Either::Left(std::iter::once(Err(
105                                LoweringError::NonWorkspaceSource(
106                                    requirement.name.clone(),
107                                    SourceKind::Url,
108                                ),
109                            )));
110                        }
111                        Source::Path { .. } => {
112                            return Either::Left(std::iter::once(Err(
113                                LoweringError::NonWorkspaceSource(
114                                    requirement.name.clone(),
115                                    SourceKind::Path,
116                                ),
117                            )));
118                        }
119                        Source::Registry { .. } => {
120                            return Either::Left(std::iter::once(Err(
121                                LoweringError::NonWorkspaceSource(
122                                    requirement.name.clone(),
123                                    SourceKind::Registry,
124                                ),
125                            )));
126                        }
127                        Source::Workspace { .. } => {
128                            // OK
129                        }
130                    }
131                }
132            }
133        }
134
135        let Some(sources) = sources else {
136            return Either::Left(std::iter::once(Ok(Self(Requirement::from(requirement)))));
137        };
138
139        // Determine whether the markers cover the full space for the requirement. If not, fill the
140        // remaining space with the negation of the sources.
141        let remaining = {
142            // Determine the space covered by the sources.
143            let mut total = MarkerTree::FALSE;
144            for source in sources.iter() {
145                total.or(source.marker());
146            }
147
148            // Determine the space covered by the requirement.
149            let mut remaining = total.negate();
150            remaining.and(requirement.marker);
151
152            Self(Requirement {
153                marker: remaining,
154                ..Requirement::from(requirement.clone())
155            })
156        };
157
158        Either::Right(
159            sources
160                .into_iter()
161                .map(move |source| {
162                    let (source, mut marker) = match source {
163                        Source::Git {
164                            git,
165                            subdirectory,
166                            rev,
167                            tag,
168                            branch,
169                            marker,
170                            ..
171                        } => {
172                            let source = git_source(
173                                &git,
174                                subdirectory.map(Box::<Path>::from),
175                                rev,
176                                tag,
177                                branch,
178                            )?;
179                            (source, marker)
180                        }
181                        Source::Url {
182                            url,
183                            subdirectory,
184                            marker,
185                            ..
186                        } => {
187                            let source =
188                                url_source(&requirement, url, subdirectory.map(Box::<Path>::from))?;
189                            (source, marker)
190                        }
191                        Source::Path {
192                            path,
193                            editable,
194                            package,
195                            marker,
196                            ..
197                        } => {
198                            let source = path_source(
199                                path,
200                                git_member,
201                                origin,
202                                project_dir,
203                                workspace.install_path(),
204                                editable,
205                                package,
206                            )?;
207                            (source, marker)
208                        }
209                        Source::Registry {
210                            index,
211                            marker,
212                            extra,
213                            group,
214                        } => {
215                            // Identify the named index from either the project indexes or the workspace indexes,
216                            // in that order.
217                            let Some(index) = locations
218                                .indexes()
219                                .filter(|index| matches!(index.origin, Some(Origin::Cli)))
220                                .chain(project_indexes.iter())
221                                .chain(workspace.indexes().iter())
222                                .find(|Index { name, .. }| {
223                                    name.as_ref().is_some_and(|name| *name == index)
224                                })
225                            else {
226                                return Err(LoweringError::MissingIndex(
227                                    requirement.name.clone(),
228                                    index,
229                                ));
230                            };
231                            if let Some(credentials) = index.credentials() {
232                                uv_auth::store_credentials(index.raw_url(), credentials);
233                            }
234                            let index = IndexMetadata {
235                                url: index.url.clone(),
236                                format: index.format,
237                            };
238                            let conflict = project_name.and_then(|project_name| {
239                                if let Some(extra) = extra {
240                                    Some(ConflictItem::from((project_name.clone(), extra)))
241                                } else {
242                                    group.map(|group| {
243                                        ConflictItem::from((project_name.clone(), group))
244                                    })
245                                }
246                            });
247                            let source = registry_source(&requirement, index, conflict);
248                            (source, marker)
249                        }
250                        Source::Workspace {
251                            workspace: is_workspace,
252                            marker,
253                            ..
254                        } => {
255                            if !is_workspace {
256                                return Err(LoweringError::WorkspaceFalse);
257                            }
258                            let member = workspace
259                                .packages()
260                                .get(&requirement.name)
261                                .ok_or_else(|| {
262                                    LoweringError::UndeclaredWorkspacePackage(
263                                        requirement.name.clone(),
264                                    )
265                                })?
266                                .clone();
267
268                            // Say we have:
269                            // ```
270                            // root
271                            // ├── main_workspace  <- We want to the path from here ...
272                            // │   ├── pyproject.toml
273                            // │   └── uv.lock
274                            // └──current_workspace
275                            //    └── packages
276                            //        └── current_package  <- ... to here.
277                            //            └── pyproject.toml
278                            // ```
279                            // The path we need in the lockfile: `../current_workspace/packages/current_project`
280                            // member root: `/root/current_workspace/packages/current_project`
281                            // workspace install root: `/root/current_workspace`
282                            // relative to workspace: `packages/current_project`
283                            // workspace lock root: `../current_workspace`
284                            // relative to main workspace: `../current_workspace/packages/current_project`
285                            let url = VerbatimUrl::from_absolute_path(member.root())?;
286                            let install_path = url.to_file_path().map_err(|()| {
287                                LoweringError::RelativeTo(io::Error::other(
288                                    "Invalid path in file URL",
289                                ))
290                            })?;
291
292                            let source = if let Some(git_member) = &git_member {
293                                // If the workspace comes from a Git dependency, all workspace
294                                // members need to be Git dependencies, too.
295                                let subdirectory =
296                                    uv_fs::relative_to(member.root(), git_member.fetch_root)
297                                        .expect("Workspace member must be relative");
298                                let subdirectory = uv_fs::normalize_path_buf(subdirectory);
299                                RequirementSource::Git {
300                                    git: git_member.git_source.git.clone(),
301                                    subdirectory: if subdirectory == PathBuf::new() {
302                                        None
303                                    } else {
304                                        Some(subdirectory.into_boxed_path())
305                                    },
306                                    url,
307                                }
308                            } else {
309                                let value = workspace.required_members().get(&requirement.name);
310                                let is_required_member = value.is_some();
311                                let editability = value.copied().flatten();
312                                if member.pyproject_toml().is_package(!is_required_member) {
313                                    RequirementSource::Directory {
314                                        install_path: install_path.into_boxed_path(),
315                                        url,
316                                        editable: Some(editability.unwrap_or(true)),
317                                        r#virtual: Some(false),
318                                    }
319                                } else {
320                                    RequirementSource::Directory {
321                                        install_path: install_path.into_boxed_path(),
322                                        url,
323                                        editable: Some(false),
324                                        r#virtual: Some(true),
325                                    }
326                                }
327                            };
328                            (source, marker)
329                        }
330                    };
331
332                    marker.and(requirement.marker);
333
334                    Ok(Self(Requirement {
335                        name: requirement.name.clone(),
336                        extras: requirement.extras.clone(),
337                        groups: Box::new([]),
338                        marker,
339                        source,
340                        origin: requirement.origin.clone(),
341                    }))
342                })
343                .chain(std::iter::once(Ok(remaining)))
344                .filter(|requirement| match requirement {
345                    Ok(requirement) => !requirement.0.marker.is_false(),
346                    Err(_) => true,
347                }),
348        )
349    }
350
351    /// Lower a [`uv_pep508::Requirement`] in a non-workspace setting (for example, in a PEP 723
352    /// script, which runs in an isolated context).
353    pub fn from_non_workspace_requirement<'data>(
354        requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
355        dir: &'data Path,
356        sources: &'data BTreeMap<PackageName, Sources>,
357        indexes: &'data [Index],
358        locations: &'data IndexLocations,
359    ) -> impl Iterator<Item = Result<Self, LoweringError>> + 'data {
360        let source = sources.get(&requirement.name).cloned();
361
362        let Some(source) = source else {
363            return Either::Left(std::iter::once(Ok(Self(Requirement::from(requirement)))));
364        };
365
366        // If the source only applies to a given extra, filter it out.
367        let source = source
368            .iter()
369            .filter(|source| {
370                source.extra().is_none_or(|target| {
371                    requirement
372                        .marker
373                        .top_level_extra_name()
374                        .is_some_and(|extra| &*extra == target)
375                })
376            })
377            .cloned()
378            .collect::<Sources>();
379
380        // Determine whether the markers cover the full space for the requirement. If not, fill the
381        // remaining space with the negation of the sources.
382        let remaining = {
383            // Determine the space covered by the sources.
384            let mut total = MarkerTree::FALSE;
385            for source in source.iter() {
386                total.or(source.marker());
387            }
388
389            // Determine the space covered by the requirement.
390            let mut remaining = total.negate();
391            remaining.and(requirement.marker);
392
393            Self(Requirement {
394                marker: remaining,
395                ..Requirement::from(requirement.clone())
396            })
397        };
398
399        Either::Right(
400            source
401                .into_iter()
402                .map(move |source| {
403                    let (source, mut marker) = match source {
404                        Source::Git {
405                            git,
406                            subdirectory,
407                            rev,
408                            tag,
409                            branch,
410                            marker,
411                            ..
412                        } => {
413                            let source = git_source(
414                                &git,
415                                subdirectory.map(Box::<Path>::from),
416                                rev,
417                                tag,
418                                branch,
419                            )?;
420                            (source, marker)
421                        }
422                        Source::Url {
423                            url,
424                            subdirectory,
425                            marker,
426                            ..
427                        } => {
428                            let source =
429                                url_source(&requirement, url, subdirectory.map(Box::<Path>::from))?;
430                            (source, marker)
431                        }
432                        Source::Path {
433                            path,
434                            editable,
435                            package,
436                            marker,
437                            ..
438                        } => {
439                            let source = path_source(
440                                path,
441                                None,
442                                RequirementOrigin::Project,
443                                dir,
444                                dir,
445                                editable,
446                                package,
447                            )?;
448                            (source, marker)
449                        }
450                        Source::Registry { index, marker, .. } => {
451                            let Some(index) = locations
452                                .indexes()
453                                .filter(|index| matches!(index.origin, Some(Origin::Cli)))
454                                .chain(indexes.iter())
455                                .find(|Index { name, .. }| {
456                                    name.as_ref().is_some_and(|name| *name == index)
457                                })
458                            else {
459                                return Err(LoweringError::MissingIndex(
460                                    requirement.name.clone(),
461                                    index,
462                                ));
463                            };
464                            if let Some(credentials) = index.credentials() {
465                                uv_auth::store_credentials(index.raw_url(), credentials);
466                            }
467                            let index = IndexMetadata {
468                                url: index.url.clone(),
469                                format: index.format,
470                            };
471                            let conflict = None;
472                            let source = registry_source(&requirement, index, conflict);
473                            (source, marker)
474                        }
475                        Source::Workspace { .. } => {
476                            return Err(LoweringError::WorkspaceMember);
477                        }
478                    };
479
480                    marker.and(requirement.marker);
481
482                    Ok(Self(Requirement {
483                        name: requirement.name.clone(),
484                        extras: requirement.extras.clone(),
485                        groups: Box::new([]),
486                        marker,
487                        source,
488                        origin: requirement.origin.clone(),
489                    }))
490                })
491                .chain(std::iter::once(Ok(remaining)))
492                .filter(|requirement| match requirement {
493                    Ok(requirement) => !requirement.0.marker.is_false(),
494                    Err(_) => true,
495                }),
496        )
497    }
498
499    /// Convert back into a [`Requirement`].
500    pub fn into_inner(self) -> Requirement {
501        self.0
502    }
503}
504
505/// An error parsing and merging `tool.uv.sources` with
506/// `project.{dependencies,optional-dependencies}`.
507#[derive(Debug, Error)]
508pub enum LoweringError {
509    #[error(
510        "`{0}` is included as a workspace member, but is missing an entry in `tool.uv.sources` (e.g., `{0} = {{ workspace = true }}`)"
511    )]
512    MissingWorkspaceSource(PackageName),
513    #[error(
514        "`{0}` is included as a workspace member, but references a {1} in `tool.uv.sources`. Workspace members must be declared as workspace sources (e.g., `{0} = {{ workspace = true }}`)."
515    )]
516    NonWorkspaceSource(PackageName, SourceKind),
517    #[error(
518        "`{0}` references a workspace in `tool.uv.sources` (e.g., `{0} = {{ workspace = true }}`), but is not a workspace member"
519    )]
520    UndeclaredWorkspacePackage(PackageName),
521    #[error("Can only specify one of: `rev`, `tag`, or `branch`")]
522    MoreThanOneGitRef,
523    #[error(transparent)]
524    GitUrlParse(#[from] GitUrlParseError),
525    #[error("Package `{0}` references an undeclared index: `{1}`")]
526    MissingIndex(PackageName, IndexName),
527    #[error("Workspace members are not allowed in non-workspace contexts")]
528    WorkspaceMember,
529    #[error(transparent)]
530    InvalidUrl(#[from] DisplaySafeUrlError),
531    #[error(transparent)]
532    InvalidVerbatimUrl(#[from] uv_pep508::VerbatimUrlError),
533    #[error("Fragments are not allowed in URLs: `{0}`")]
534    ForbiddenFragment(DisplaySafeUrl),
535    #[error(
536        "`{0}` is associated with a URL source, but references a Git repository. Consider using a Git source instead (e.g., `{0} = {{ git = \"{1}\" }}`)"
537    )]
538    MissingGitSource(PackageName, DisplaySafeUrl),
539    #[error("`workspace = false` is not yet supported")]
540    WorkspaceFalse,
541    #[error("Source with `editable = true` must refer to a local directory, not a file: `{0}`")]
542    EditableFile(String),
543    #[error("Source with `package = true` must refer to a local directory, not a file: `{0}`")]
544    PackagedFile(String),
545    #[error(
546        "Git repository references local file source, but only directories are supported as transitive Git dependencies: `{0}`"
547    )]
548    GitFile(String),
549    #[error(transparent)]
550    ParsedUrl(#[from] ParsedUrlError),
551    #[error("Path must be UTF-8: `{0}`")]
552    NonUtf8Path(PathBuf),
553    #[error(transparent)] // Function attaches the context
554    RelativeTo(io::Error),
555}
556
557#[derive(Debug, Copy, Clone)]
558pub enum SourceKind {
559    Path,
560    Url,
561    Git,
562    Registry,
563}
564
565impl std::fmt::Display for SourceKind {
566    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
567        match self {
568            Self::Path => write!(f, "path"),
569            Self::Url => write!(f, "URL"),
570            Self::Git => write!(f, "Git"),
571            Self::Registry => write!(f, "registry"),
572        }
573    }
574}
575
576/// Convert a Git source into a [`RequirementSource`].
577fn git_source(
578    git: &DisplaySafeUrl,
579    subdirectory: Option<Box<Path>>,
580    rev: Option<String>,
581    tag: Option<String>,
582    branch: Option<String>,
583) -> Result<RequirementSource, LoweringError> {
584    let reference = match (rev, tag, branch) {
585        (None, None, None) => GitReference::DefaultBranch,
586        (Some(rev), None, None) => GitReference::from_rev(rev),
587        (None, Some(tag), None) => GitReference::Tag(tag),
588        (None, None, Some(branch)) => GitReference::Branch(branch),
589        _ => return Err(LoweringError::MoreThanOneGitRef),
590    };
591
592    // Create a PEP 508-compatible URL.
593    let mut url = DisplaySafeUrl::parse(&format!("git+{git}"))?;
594    if let Some(rev) = reference.as_str() {
595        let path = format!("{}@{}", url.path(), rev);
596        url.set_path(&path);
597    }
598    if let Some(subdirectory) = subdirectory.as_ref() {
599        let subdirectory = subdirectory
600            .to_str()
601            .ok_or_else(|| LoweringError::NonUtf8Path(subdirectory.to_path_buf()))?;
602        url.set_fragment(Some(&format!("subdirectory={subdirectory}")));
603    }
604    let url = VerbatimUrl::from_url(url);
605
606    let repository = git.clone();
607
608    Ok(RequirementSource::Git {
609        url,
610        git: GitUrl::from_reference(repository, reference)?,
611        subdirectory,
612    })
613}
614
615/// Convert a URL source into a [`RequirementSource`].
616fn url_source(
617    requirement: &uv_pep508::Requirement<VerbatimParsedUrl>,
618    url: DisplaySafeUrl,
619    subdirectory: Option<Box<Path>>,
620) -> Result<RequirementSource, LoweringError> {
621    let mut verbatim_url = url.clone();
622    if verbatim_url.fragment().is_some() {
623        return Err(LoweringError::ForbiddenFragment(url));
624    }
625    if let Some(subdirectory) = subdirectory.as_ref() {
626        let subdirectory = subdirectory
627            .to_str()
628            .ok_or_else(|| LoweringError::NonUtf8Path(subdirectory.to_path_buf()))?;
629        verbatim_url.set_fragment(Some(&format!("subdirectory={subdirectory}")));
630    }
631
632    let ext = match DistExtension::from_path(url.path()) {
633        Ok(ext) => ext,
634        Err(..) if looks_like_git_repository(&url) => {
635            return Err(LoweringError::MissingGitSource(
636                requirement.name.clone(),
637                url.clone(),
638            ));
639        }
640        Err(err) => {
641            return Err(ParsedUrlError::MissingExtensionUrl(url.to_string(), err).into());
642        }
643    };
644
645    let verbatim_url = VerbatimUrl::from_url(verbatim_url);
646    Ok(RequirementSource::Url {
647        location: url,
648        subdirectory,
649        ext,
650        url: verbatim_url,
651    })
652}
653
654/// Convert a registry source into a [`RequirementSource`].
655fn registry_source(
656    requirement: &uv_pep508::Requirement<VerbatimParsedUrl>,
657    index: IndexMetadata,
658    conflict: Option<ConflictItem>,
659) -> RequirementSource {
660    match &requirement.version_or_url {
661        None => RequirementSource::Registry {
662            specifier: VersionSpecifiers::empty(),
663            index: Some(index),
664            conflict,
665        },
666        Some(VersionOrUrl::VersionSpecifier(version)) => RequirementSource::Registry {
667            specifier: version.clone(),
668            index: Some(index),
669            conflict,
670        },
671        Some(VersionOrUrl::Url(_)) => RequirementSource::Registry {
672            specifier: VersionSpecifiers::empty(),
673            index: Some(index),
674            conflict,
675        },
676    }
677}
678
679/// Convert a path string to a file or directory source.
680fn path_source(
681    path: impl AsRef<Path>,
682    git_member: Option<&GitWorkspaceMember>,
683    origin: RequirementOrigin,
684    project_dir: &Path,
685    workspace_root: &Path,
686    editable: Option<bool>,
687    package: Option<bool>,
688) -> Result<RequirementSource, LoweringError> {
689    let path = path.as_ref();
690    let base = match origin {
691        RequirementOrigin::Project => project_dir,
692        RequirementOrigin::Workspace => workspace_root,
693    };
694    let url = VerbatimUrl::from_path(path, base)?.with_given(path.to_string_lossy());
695    let install_path = url
696        .to_file_path()
697        .map_err(|()| LoweringError::RelativeTo(io::Error::other("Invalid path in file URL")))?;
698
699    let is_dir = if let Ok(metadata) = install_path.metadata() {
700        metadata.is_dir()
701    } else {
702        install_path.extension().is_none()
703    };
704    if is_dir {
705        if let Some(git_member) = git_member {
706            let git = git_member.git_source.git.clone();
707            let subdirectory = uv_fs::relative_to(install_path, git_member.fetch_root)
708                .expect("Workspace member must be relative");
709            let subdirectory = uv_fs::normalize_path_buf(subdirectory);
710            let subdirectory = if subdirectory == PathBuf::new() {
711                None
712            } else {
713                Some(subdirectory.into_boxed_path())
714            };
715            let url = DisplaySafeUrl::from(ParsedGitUrl {
716                url: git.clone(),
717                subdirectory: subdirectory.clone(),
718            });
719            return Ok(RequirementSource::Git {
720                git,
721                subdirectory,
722                url: VerbatimUrl::from_url(url),
723            });
724        }
725
726        if editable == Some(true) {
727            Ok(RequirementSource::Directory {
728                install_path: install_path.into_boxed_path(),
729                url,
730                editable,
731                r#virtual: Some(false),
732            })
733        } else {
734            // Determine whether the project is a package or virtual.
735            // If the `package` option is unset, check if `tool.uv.package` is set
736            // on the path source (otherwise, default to `true`).
737            let is_package = package.unwrap_or_else(|| {
738                let pyproject_path = install_path.join("pyproject.toml");
739                fs_err::read_to_string(&pyproject_path)
740                    .ok()
741                    .and_then(|contents| PyProjectToml::from_string(contents).ok())
742                    // We don't require a build system for path dependencies
743                    .map(|pyproject_toml| pyproject_toml.is_package(false))
744                    .unwrap_or(true)
745            });
746
747            // If the project is not a package, treat it as a virtual dependency.
748            let r#virtual = !is_package;
749
750            Ok(RequirementSource::Directory {
751                install_path: install_path.into_boxed_path(),
752                url,
753                editable: Some(false),
754                r#virtual: Some(r#virtual),
755            })
756        }
757    } else {
758        // TODO(charlie): If a Git repo contains a source that points to a file, what should we do?
759        if git_member.is_some() {
760            return Err(LoweringError::GitFile(url.to_string()));
761        }
762        if editable == Some(true) {
763            return Err(LoweringError::EditableFile(url.to_string()));
764        }
765        if package == Some(true) {
766            return Err(LoweringError::PackagedFile(url.to_string()));
767        }
768        Ok(RequirementSource::Path {
769            ext: DistExtension::from_path(&install_path)
770                .map_err(|err| ParsedUrlError::MissingExtensionPath(path.to_path_buf(), err))?,
771            install_path: install_path.into_boxed_path(),
772            url,
773        })
774    }
775}