python_packaging/
resource.rs

1// Copyright 2022 Gregory Szorc.
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9/*! Defines types representing Python resources. */
10
11use {
12    crate::{
13        bytecode::{CompileMode, PythonBytecodeCompiler},
14        licensing::LicensedComponent,
15        module_util::{is_package_from_path, packages_from_module_name, resolve_path_for_module},
16        python_source::has_dunder_file,
17    },
18    anyhow::{anyhow, Result},
19    simple_file_manifest::{File, FileData},
20    std::{
21        borrow::Cow,
22        collections::HashMap,
23        hash::BuildHasher,
24        path::{Path, PathBuf},
25    },
26};
27
28#[cfg(feature = "serialization")]
29use serde::{Deserialize, Serialize};
30
31/// An optimization level for Python bytecode.
32///
33/// Serialization type: `int`
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35#[cfg_attr(feature = "serialization", derive(Deserialize, Serialize))]
36pub enum BytecodeOptimizationLevel {
37    /// Optimization level 0.
38    ///
39    /// Serialized value: `0`
40    #[cfg_attr(feature = "serialization", serde(rename = "0"))]
41    Zero,
42
43    /// Optimization level 1.
44    ///
45    /// Serialized value: `1`
46    #[cfg_attr(feature = "serialization", serde(rename = "1"))]
47    One,
48
49    /// Optimization level 2.
50    ///
51    /// Serialized value: `2`
52    #[cfg_attr(feature = "serialization", serde(rename = "2"))]
53    Two,
54}
55
56impl BytecodeOptimizationLevel {
57    /// Determine hte extra filename tag for bytecode files of this variant.
58    pub fn to_extra_tag(&self) -> &'static str {
59        match self {
60            BytecodeOptimizationLevel::Zero => "",
61            BytecodeOptimizationLevel::One => ".opt-1",
62            BytecodeOptimizationLevel::Two => ".opt-2",
63        }
64    }
65}
66
67impl TryFrom<i32> for BytecodeOptimizationLevel {
68    type Error = &'static str;
69
70    fn try_from(i: i32) -> Result<Self, Self::Error> {
71        match i {
72            0 => Ok(BytecodeOptimizationLevel::Zero),
73            1 => Ok(BytecodeOptimizationLevel::One),
74            2 => Ok(BytecodeOptimizationLevel::Two),
75            _ => Err("unsupported bytecode optimization level"),
76        }
77    }
78}
79
80impl From<BytecodeOptimizationLevel> for i32 {
81    fn from(level: BytecodeOptimizationLevel) -> Self {
82        match level {
83            BytecodeOptimizationLevel::Zero => 0,
84            BytecodeOptimizationLevel::One => 1,
85            BytecodeOptimizationLevel::Two => 2,
86        }
87    }
88}
89
90/// A Python module defined via source code.
91#[derive(Clone, Debug, PartialEq)]
92pub struct PythonModuleSource {
93    /// The fully qualified Python module name.
94    pub name: String,
95    /// Python source code.
96    pub source: FileData,
97    /// Whether this module is also a package.
98    pub is_package: bool,
99    /// Tag to apply to bytecode files.
100    ///
101    /// e.g. `cpython-39`.
102    pub cache_tag: String,
103    /// Whether this module belongs to the Python standard library.
104    ///
105    /// Modules with this set are distributed as part of Python itself.
106    pub is_stdlib: bool,
107    /// Whether this module is a test module.
108    ///
109    /// Test modules are those defining test code and aren't critical to
110    /// run-time functionality of a package.
111    pub is_test: bool,
112}
113
114impl PythonModuleSource {
115    pub fn description(&self) -> String {
116        format!("source code for Python module {}", self.name)
117    }
118
119    pub fn to_memory(&self) -> Result<Self> {
120        Ok(Self {
121            name: self.name.clone(),
122            source: self.source.to_memory()?,
123            is_package: self.is_package,
124            cache_tag: self.cache_tag.clone(),
125            is_stdlib: self.is_stdlib,
126            is_test: self.is_test,
127        })
128    }
129
130    /// Resolve the package containing this module.
131    ///
132    /// If this module is a package, returns the name of self.
133    pub fn package(&self) -> String {
134        if self.is_package {
135            self.name.clone()
136        } else if let Some(idx) = self.name.rfind('.') {
137            self.name[0..idx].to_string()
138        } else {
139            self.name.clone()
140        }
141    }
142
143    /// Obtain the top-level package name this module belongs to.
144    pub fn top_level_package(&self) -> &str {
145        if let Some(idx) = self.name.find('.') {
146            &self.name[0..idx]
147        } else {
148            &self.name
149        }
150    }
151
152    /// Convert the instance to a BytecodeModule.
153    pub fn as_bytecode_module(
154        &self,
155        optimize_level: BytecodeOptimizationLevel,
156    ) -> PythonModuleBytecodeFromSource {
157        PythonModuleBytecodeFromSource {
158            name: self.name.clone(),
159            source: self.source.clone(),
160            optimize_level,
161            is_package: self.is_package,
162            cache_tag: self.cache_tag.clone(),
163            is_stdlib: self.is_stdlib,
164            is_test: self.is_test,
165        }
166    }
167
168    /// Resolve the filesystem path for this source module.
169    pub fn resolve_path(&self, prefix: &str) -> PathBuf {
170        resolve_path_for_module(prefix, &self.name, self.is_package, None)
171    }
172
173    /// Whether the source code for this module has __file__
174    pub fn has_dunder_file(&self) -> Result<bool> {
175        has_dunder_file(&self.source.resolve_content()?)
176    }
177}
178
179/// Python module bytecode defined via source code.
180///
181/// This is essentially a request to generate bytecode from Python module
182/// source code.
183#[derive(Clone, Debug, PartialEq)]
184pub struct PythonModuleBytecodeFromSource {
185    pub name: String,
186    pub source: FileData,
187    pub optimize_level: BytecodeOptimizationLevel,
188    pub is_package: bool,
189    /// Tag to apply to bytecode files.
190    ///
191    /// e.g. `cpython-39`.
192    pub cache_tag: String,
193    /// Whether this module belongs to the Python standard library.
194    ///
195    /// Modules with this set are distributed as part of Python itself.
196    pub is_stdlib: bool,
197    /// Whether this module is a test module.
198    ///
199    /// Test modules are those defining test code and aren't critical to
200    /// run-time functionality of a package.
201    pub is_test: bool,
202}
203
204impl PythonModuleBytecodeFromSource {
205    pub fn description(&self) -> String {
206        format!(
207            "bytecode for Python module {} at O{} (compiled from source)",
208            self.name, self.optimize_level as i32
209        )
210    }
211
212    pub fn to_memory(&self) -> Result<Self> {
213        Ok(Self {
214            name: self.name.clone(),
215            source: self.source.to_memory()?,
216            optimize_level: self.optimize_level,
217            is_package: self.is_package,
218            cache_tag: self.cache_tag.clone(),
219            is_stdlib: self.is_stdlib,
220            is_test: self.is_test,
221        })
222    }
223
224    /// Compile source to bytecode using a compiler.
225    pub fn compile(
226        &self,
227        compiler: &mut dyn PythonBytecodeCompiler,
228        mode: CompileMode,
229    ) -> Result<Vec<u8>> {
230        compiler.compile(
231            &self.source.resolve_content()?,
232            &self.name,
233            self.optimize_level,
234            mode,
235        )
236    }
237
238    /// Resolve filesystem path to this bytecode.
239    pub fn resolve_path(&self, prefix: &str) -> PathBuf {
240        let bytecode_tag = match self.optimize_level {
241            BytecodeOptimizationLevel::Zero => self.cache_tag.clone(),
242            BytecodeOptimizationLevel::One => format!("{}.opt-1", self.cache_tag),
243            BytecodeOptimizationLevel::Two => format!("{}.opt-2", self.cache_tag),
244        };
245
246        resolve_path_for_module(prefix, &self.name, self.is_package, Some(&bytecode_tag))
247    }
248
249    /// Whether the source for this module has __file__.
250    pub fn has_dunder_file(&self) -> Result<bool> {
251        has_dunder_file(&self.source.resolve_content()?)
252    }
253}
254
255/// Compiled Python module bytecode.
256#[derive(Clone, Debug, PartialEq)]
257pub struct PythonModuleBytecode {
258    pub name: String,
259    bytecode: FileData,
260    pub optimize_level: BytecodeOptimizationLevel,
261    pub is_package: bool,
262    /// Tag to apply to bytecode files.
263    ///
264    /// e.g. `cpython-39`.
265    pub cache_tag: String,
266    /// Whether this module belongs to the Python standard library.
267    ///
268    /// Modules with this set are distributed as part of Python itself.
269    pub is_stdlib: bool,
270    /// Whether this module is a test module.
271    ///
272    /// Test modules are those defining test code and aren't critical to
273    /// run-time functionality of a package.
274    pub is_test: bool,
275}
276
277impl PythonModuleBytecode {
278    pub fn new(
279        name: &str,
280        optimize_level: BytecodeOptimizationLevel,
281        is_package: bool,
282        cache_tag: &str,
283        data: &[u8],
284    ) -> Self {
285        Self {
286            name: name.to_string(),
287            bytecode: FileData::Memory(data.to_vec()),
288            optimize_level,
289            is_package,
290            cache_tag: cache_tag.to_string(),
291            is_stdlib: false,
292            is_test: false,
293        }
294    }
295
296    pub fn from_path(
297        name: &str,
298        optimize_level: BytecodeOptimizationLevel,
299        cache_tag: &str,
300        path: &Path,
301    ) -> Self {
302        Self {
303            name: name.to_string(),
304            bytecode: FileData::Path(path.to_path_buf()),
305            optimize_level,
306            is_package: is_package_from_path(path),
307            cache_tag: cache_tag.to_string(),
308            is_stdlib: false,
309            is_test: false,
310        }
311    }
312
313    pub fn description(&self) -> String {
314        format!(
315            "bytecode for Python module {} at O{}",
316            self.name, self.optimize_level as i32
317        )
318    }
319
320    pub fn to_memory(&self) -> Result<Self> {
321        Ok(Self {
322            name: self.name.clone(),
323            bytecode: FileData::Memory(self.resolve_bytecode()?),
324            optimize_level: self.optimize_level,
325            is_package: self.is_package,
326            cache_tag: self.cache_tag.clone(),
327            is_stdlib: self.is_stdlib,
328            is_test: self.is_test,
329        })
330    }
331
332    /// Resolve the bytecode data for this module.
333    pub fn resolve_bytecode(&self) -> Result<Vec<u8>> {
334        match &self.bytecode {
335            FileData::Memory(data) => Ok(data.clone()),
336            FileData::Path(path) => {
337                let data = std::fs::read(path)?;
338
339                if data.len() >= 16 {
340                    Ok(data[16..data.len()].to_vec())
341                } else {
342                    Err(anyhow!("bytecode file is too short"))
343                }
344            }
345        }
346    }
347
348    /// Sets the bytecode for this module.
349    pub fn set_bytecode(&mut self, data: &[u8]) {
350        self.bytecode = FileData::Memory(data.to_vec());
351    }
352
353    /// Resolve filesystem path to this bytecode.
354    pub fn resolve_path(&self, prefix: &str) -> PathBuf {
355        let bytecode_tag = match self.optimize_level {
356            BytecodeOptimizationLevel::Zero => self.cache_tag.clone(),
357            BytecodeOptimizationLevel::One => format!("{}.opt-1", self.cache_tag),
358            BytecodeOptimizationLevel::Two => format!("{}.opt-2", self.cache_tag),
359        };
360
361        resolve_path_for_module(prefix, &self.name, self.is_package, Some(&bytecode_tag))
362    }
363}
364
365/// Python package resource data, agnostic of storage location.
366#[derive(Clone, Debug, PartialEq)]
367pub struct PythonPackageResource {
368    /// The leaf-most Python package this resource belongs to.
369    pub leaf_package: String,
370    /// The relative path within `leaf_package` to this resource.
371    pub relative_name: String,
372    /// Location of resource data.
373    pub data: FileData,
374    /// Whether this resource belongs to the Python standard library.
375    ///
376    /// Modules with this set are distributed as part of Python itself.
377    pub is_stdlib: bool,
378    /// Whether this resource belongs to a package that is a test.
379    pub is_test: bool,
380}
381
382impl PythonPackageResource {
383    pub fn description(&self) -> String {
384        format!("Python package resource {}", self.symbolic_name())
385    }
386
387    pub fn to_memory(&self) -> Result<Self> {
388        Ok(Self {
389            leaf_package: self.leaf_package.clone(),
390            relative_name: self.relative_name.clone(),
391            data: self.data.to_memory()?,
392            is_stdlib: self.is_stdlib,
393            is_test: self.is_test,
394        })
395    }
396
397    pub fn symbolic_name(&self) -> String {
398        format!("{}:{}", self.leaf_package, self.relative_name)
399    }
400
401    /// Resolve filesystem path to this bytecode.
402    pub fn resolve_path(&self, prefix: &str) -> PathBuf {
403        let mut path = PathBuf::from(prefix);
404
405        for p in self.leaf_package.split('.') {
406            path = path.join(p);
407        }
408
409        path = path.join(&self.relative_name);
410
411        path
412    }
413}
414
415/// Represents where a Python package distribution resource is materialized.
416#[derive(Clone, Debug, PartialEq, Eq)]
417pub enum PythonPackageDistributionResourceFlavor {
418    /// In a .dist-info directory.
419    DistInfo,
420
421    /// In a .egg-info directory.
422    EggInfo,
423}
424
425/// Represents a file defining Python package metadata.
426///
427/// Instances of this correspond to files in a `<package>-<version>.dist-info`
428/// or `.egg-info` directory.
429///
430/// In terms of `importlib.metadata` terminology, instances correspond to
431/// files in a `Distribution`.
432#[derive(Clone, Debug, PartialEq)]
433pub struct PythonPackageDistributionResource {
434    /// Where the resource is materialized.
435    pub location: PythonPackageDistributionResourceFlavor,
436
437    /// The name of the Python package this resource is associated with.
438    pub package: String,
439
440    /// Version string of Python package.
441    pub version: String,
442
443    /// Name of this resource within the distribution.
444    ///
445    /// Corresponds to the file name in the `.dist-info` directory for this
446    /// package distribution.
447    pub name: String,
448
449    /// The raw content of the distribution resource.
450    pub data: FileData,
451}
452
453impl PythonPackageDistributionResource {
454    pub fn description(&self) -> String {
455        format!(
456            "Python package distribution resource {}:{}",
457            self.package, self.name
458        )
459    }
460
461    pub fn to_memory(&self) -> Result<Self> {
462        Ok(Self {
463            location: self.location.clone(),
464            package: self.package.clone(),
465            version: self.version.clone(),
466            name: self.name.clone(),
467            data: self.data.to_memory()?,
468        })
469    }
470
471    /// Resolve filesystem path to this resource file.
472    pub fn resolve_path(&self, prefix: &str) -> PathBuf {
473        // The package name has hyphens normalized to underscores when
474        // materialized on the filesystem.
475        let normalized_package = self.package.to_lowercase().replace('-', "_");
476
477        let p = match self.location {
478            PythonPackageDistributionResourceFlavor::DistInfo => {
479                format!("{}-{}.dist-info", normalized_package, self.version)
480            }
481            PythonPackageDistributionResourceFlavor::EggInfo => {
482                format!("{}-{}.egg-info", normalized_package, self.version)
483            }
484        };
485
486        PathBuf::from(prefix).join(p).join(&self.name)
487    }
488}
489
490/// Represents a dependency on a library.
491///
492/// The library can be defined a number of ways and multiple variants may be
493/// present.
494#[derive(Clone, Debug, PartialEq)]
495pub struct LibraryDependency {
496    /// Name of the library.
497    ///
498    /// This will be used to tell the linker what to link.
499    pub name: String,
500
501    /// Static library version of library.
502    pub static_library: Option<FileData>,
503
504    /// The filename the static library should be materialized as.
505    pub static_filename: Option<PathBuf>,
506
507    /// Shared library version of library.
508    pub dynamic_library: Option<FileData>,
509
510    /// The filename the dynamic library should be materialized as.
511    pub dynamic_filename: Option<PathBuf>,
512
513    /// Whether this is a system framework (macOS).
514    pub framework: bool,
515
516    /// Whether this is a system library.
517    pub system: bool,
518}
519
520impl LibraryDependency {
521    pub fn to_memory(&self) -> Result<Self> {
522        Ok(Self {
523            name: self.name.clone(),
524            static_library: if let Some(data) = &self.static_library {
525                Some(data.to_memory()?)
526            } else {
527                None
528            },
529            static_filename: self.static_filename.clone(),
530            dynamic_library: if let Some(data) = &self.dynamic_library {
531                Some(data.to_memory()?)
532            } else {
533                None
534            },
535            dynamic_filename: self.dynamic_filename.clone(),
536            framework: self.framework,
537            system: self.system,
538        })
539    }
540}
541
542/// Represents a shared library.
543#[derive(Clone, Debug, PartialEq)]
544pub struct SharedLibrary {
545    /// Name of the library.
546    ///
547    /// This is the import name, not the full filename.
548    pub name: String,
549
550    /// Holds the raw content of the shared library.
551    pub data: FileData,
552
553    /// The filename the library should be materialized as.
554    pub filename: Option<PathBuf>,
555}
556
557impl TryFrom<&LibraryDependency> for SharedLibrary {
558    type Error = &'static str;
559
560    fn try_from(value: &LibraryDependency) -> Result<Self, Self::Error> {
561        if let Some(data) = &value.dynamic_library {
562            Ok(Self {
563                name: value.name.clone(),
564                data: data.clone(),
565                filename: value.dynamic_filename.clone(),
566            })
567        } else {
568            Err("library dependency does not have a shared library")
569        }
570    }
571}
572
573impl SharedLibrary {
574    pub fn description(&self) -> String {
575        format!("shared library {}", self.name)
576    }
577}
578
579/// Represents a Python extension module.
580#[derive(Clone, Debug, PartialEq)]
581pub struct PythonExtensionModule {
582    /// The module name this extension module is providing.
583    pub name: String,
584    /// Name of the C function initializing this extension module.
585    pub init_fn: Option<String>,
586    /// Filename suffix to use when writing extension module data.
587    pub extension_file_suffix: String,
588    /// File data for linked extension module.
589    pub shared_library: Option<FileData>,
590    // TODO capture static library?
591    /// File data for object files linked together to produce this extension module.
592    pub object_file_data: Vec<FileData>,
593    /// Whether this extension module is a package.
594    pub is_package: bool,
595    /// Libraries that this extension depends on.
596    pub link_libraries: Vec<LibraryDependency>,
597    /// Whether this extension module is part of the Python standard library.
598    ///
599    /// This is true if the extension is distributed with Python itself.
600    pub is_stdlib: bool,
601    /// Whether the extension module is built-in by default.
602    ///
603    /// Some extension modules in Python distributions are always compiled into
604    /// libpython. This field will be true for those extension modules.
605    pub builtin_default: bool,
606    /// Whether the extension must be loaded to initialize Python.
607    pub required: bool,
608    /// Name of the variant of this extension module.
609    ///
610    /// This may be set if there are multiple versions of an extension module
611    /// available to choose from.
612    pub variant: Option<String>,
613    /// Licenses that apply to this extension.
614    pub license: Option<LicensedComponent>,
615}
616
617impl PythonExtensionModule {
618    pub fn description(&self) -> String {
619        format!("Python extension module {}", self.name)
620    }
621
622    pub fn to_memory(&self) -> Result<Self> {
623        Ok(Self {
624            name: self.name.clone(),
625            init_fn: self.init_fn.clone(),
626            extension_file_suffix: self.extension_file_suffix.clone(),
627            shared_library: if let Some(data) = &self.shared_library {
628                Some(data.to_memory()?)
629            } else {
630                None
631            },
632            object_file_data: self.object_file_data.clone(),
633            is_package: self.is_package,
634            link_libraries: self
635                .link_libraries
636                .iter()
637                .map(|l| l.to_memory())
638                .collect::<Result<Vec<_>, _>>()?,
639            is_stdlib: self.is_stdlib,
640            builtin_default: self.builtin_default,
641            required: self.required,
642            variant: self.variant.clone(),
643            license: self.license.clone(),
644        })
645    }
646
647    /// The file name (without parent components) this extension module should be
648    /// realized with.
649    pub fn file_name(&self) -> String {
650        if let Some(idx) = self.name.rfind('.') {
651            let name = &self.name[idx + 1..self.name.len()];
652            format!("{}{}", name, self.extension_file_suffix)
653        } else {
654            format!("{}{}", self.name, self.extension_file_suffix)
655        }
656    }
657
658    /// Resolve the filesystem path for this extension module.
659    pub fn resolve_path(&self, prefix: &str) -> PathBuf {
660        let mut path = PathBuf::from(prefix);
661        path.extend(self.package_parts());
662        path.push(self.file_name());
663
664        path
665    }
666
667    /// Returns the part strings constituting the package name.
668    pub fn package_parts(&self) -> Vec<String> {
669        if let Some(idx) = self.name.rfind('.') {
670            let prefix = &self.name[0..idx];
671            prefix.split('.').map(|x| x.to_string()).collect()
672        } else {
673            Vec::new()
674        }
675    }
676
677    /// Whether the extension module requires additional libraries.
678    pub fn requires_libraries(&self) -> bool {
679        !self.link_libraries.is_empty()
680    }
681
682    /// Whether the extension module is minimally required for a Python interpreter.
683    ///
684    /// This will be true only for extension modules in the standard library that
685    /// are builtins part of libpython or are required as part of Python interpreter
686    /// initialization.
687    pub fn is_minimally_required(&self) -> bool {
688        self.is_stdlib && (self.builtin_default || self.required)
689    }
690
691    /// Whether this extension module is already in libpython.
692    ///
693    /// This is true if this is a stdlib extension module and is a core module or no
694    /// shared library extension module is available.
695    pub fn in_libpython(&self) -> bool {
696        self.is_stdlib && (self.builtin_default || self.shared_library.is_none())
697    }
698
699    /// Obtain the top-level package name this module belongs to.
700    pub fn top_level_package(&self) -> &str {
701        if let Some(idx) = self.name.find('.') {
702            &self.name[0..idx]
703        } else {
704            &self.name
705        }
706    }
707}
708
709/// Represents a collection of variants for a given Python extension module.
710#[derive(Clone, Debug, Default)]
711pub struct PythonExtensionModuleVariants {
712    extensions: Vec<PythonExtensionModule>,
713}
714
715impl FromIterator<PythonExtensionModule> for PythonExtensionModuleVariants {
716    fn from_iter<I: IntoIterator<Item = PythonExtensionModule>>(iter: I) -> Self {
717        Self {
718            extensions: Vec::from_iter(iter),
719        }
720    }
721}
722
723impl PythonExtensionModuleVariants {
724    pub fn push(&mut self, em: PythonExtensionModule) {
725        self.extensions.push(em);
726    }
727
728    pub fn is_empty(&self) -> bool {
729        self.extensions.is_empty()
730    }
731
732    pub fn iter(&self) -> impl Iterator<Item = &PythonExtensionModule> {
733        self.extensions.iter()
734    }
735
736    /// Obtains the default / first variant of an extension module.
737    pub fn default_variant(&self) -> &PythonExtensionModule {
738        &self.extensions[0]
739    }
740
741    /// Choose a variant given preferences.
742    pub fn choose_variant<S: BuildHasher>(
743        &self,
744        variants: &HashMap<String, String, S>,
745    ) -> &PythonExtensionModule {
746        // The default / first item is the chosen one by default.
747        let mut chosen = self.default_variant();
748
749        // But it can be overridden if we passed in a hash defining variant
750        // preferences, the hash contains a key with the extension name, and the
751        // requested variant value exists.
752        if let Some(preferred) = variants.get(&chosen.name) {
753            for em in self.iter() {
754                if em.variant == Some(preferred.to_string()) {
755                    chosen = em;
756                    break;
757                }
758            }
759        }
760
761        chosen
762    }
763}
764
765/// Represents a Python .egg file.
766#[derive(Clone, Debug, PartialEq)]
767pub struct PythonEggFile {
768    /// Content of the .egg file.
769    pub data: FileData,
770}
771
772impl PythonEggFile {
773    pub fn to_memory(&self) -> Result<Self> {
774        Ok(Self {
775            data: self.data.to_memory()?,
776        })
777    }
778}
779
780/// Represents a Python path extension.
781///
782/// i.e. a .pth file.
783#[derive(Clone, Debug, PartialEq)]
784pub struct PythonPathExtension {
785    /// Content of the .pth file.
786    pub data: FileData,
787}
788
789impl PythonPathExtension {
790    pub fn to_memory(&self) -> Result<Self> {
791        Ok(Self {
792            data: self.data.to_memory()?,
793        })
794    }
795}
796
797/// Represents a resource that can be read by Python somehow.
798#[allow(clippy::large_enum_variant)]
799#[derive(Clone, Debug, PartialEq)]
800pub enum PythonResource<'a> {
801    /// A module defined by source code.
802    ModuleSource(Cow<'a, PythonModuleSource>),
803    /// A module defined by a request to generate bytecode from source.
804    ModuleBytecodeRequest(Cow<'a, PythonModuleBytecodeFromSource>),
805    /// A module defined by existing bytecode.
806    ModuleBytecode(Cow<'a, PythonModuleBytecode>),
807    /// A non-module resource file.
808    PackageResource(Cow<'a, PythonPackageResource>),
809    /// A file in a Python package distribution metadata collection.
810    PackageDistributionResource(Cow<'a, PythonPackageDistributionResource>),
811    /// An extension module.
812    ExtensionModule(Cow<'a, PythonExtensionModule>),
813    /// A self-contained Python egg.
814    EggFile(Cow<'a, PythonEggFile>),
815    /// A path extension.
816    PathExtension(Cow<'a, PythonPathExtension>),
817    /// An arbitrary file and its data.
818    File(Cow<'a, File>),
819}
820
821impl<'a> PythonResource<'a> {
822    /// Resolves the fully qualified resource name.
823    pub fn full_name(&self) -> String {
824        match self {
825            PythonResource::ModuleSource(m) => m.name.clone(),
826            PythonResource::ModuleBytecode(m) => m.name.clone(),
827            PythonResource::ModuleBytecodeRequest(m) => m.name.clone(),
828            PythonResource::PackageResource(resource) => {
829                format!("{}.{}", resource.leaf_package, resource.relative_name)
830            }
831            PythonResource::PackageDistributionResource(resource) => {
832                format!("{}:{}", resource.package, resource.name)
833            }
834            PythonResource::ExtensionModule(em) => em.name.clone(),
835            PythonResource::EggFile(_) => "".to_string(),
836            PythonResource::PathExtension(_) => "".to_string(),
837            PythonResource::File(f) => format!("{}", f.path().display()),
838        }
839    }
840
841    pub fn is_in_packages(&self, packages: &[String]) -> bool {
842        let name = match self {
843            PythonResource::ModuleSource(m) => &m.name,
844            PythonResource::ModuleBytecode(m) => &m.name,
845            PythonResource::ModuleBytecodeRequest(m) => &m.name,
846            PythonResource::PackageResource(resource) => &resource.leaf_package,
847            PythonResource::PackageDistributionResource(resource) => &resource.package,
848            PythonResource::ExtensionModule(em) => &em.name,
849            PythonResource::EggFile(_) => return false,
850            PythonResource::PathExtension(_) => return false,
851            PythonResource::File(_) => return false,
852        };
853
854        for package in packages {
855            // Even though the entity may not be marked as a package, we allow exact
856            // name matches through the filter because this makes sense for filtering.
857            // The package annotation is really only useful to influence file layout,
858            // when __init__.py files need to be materialized.
859            if name == package || packages_from_module_name(name).contains(package) {
860                return true;
861            }
862        }
863
864        false
865    }
866
867    /// Create a new instance that is guaranteed to be backed by memory.
868    pub fn to_memory(&self) -> Result<Self> {
869        Ok(match self {
870            PythonResource::ModuleSource(m) => m.to_memory()?.into(),
871            PythonResource::ModuleBytecode(m) => m.to_memory()?.into(),
872            PythonResource::ModuleBytecodeRequest(m) => m.to_memory()?.into(),
873            PythonResource::PackageResource(r) => r.to_memory()?.into(),
874            PythonResource::PackageDistributionResource(r) => r.to_memory()?.into(),
875            PythonResource::ExtensionModule(m) => m.to_memory()?.into(),
876            PythonResource::EggFile(e) => e.to_memory()?.into(),
877            PythonResource::PathExtension(e) => e.to_memory()?.into(),
878            PythonResource::File(f) => f.to_memory()?.into(),
879        })
880    }
881}
882
883impl<'a> From<PythonModuleSource> for PythonResource<'a> {
884    fn from(m: PythonModuleSource) -> Self {
885        PythonResource::ModuleSource(Cow::Owned(m))
886    }
887}
888
889impl<'a> From<&'a PythonModuleSource> for PythonResource<'a> {
890    fn from(m: &'a PythonModuleSource) -> Self {
891        PythonResource::ModuleSource(Cow::Borrowed(m))
892    }
893}
894
895impl<'a> From<PythonModuleBytecodeFromSource> for PythonResource<'a> {
896    fn from(m: PythonModuleBytecodeFromSource) -> Self {
897        PythonResource::ModuleBytecodeRequest(Cow::Owned(m))
898    }
899}
900
901impl<'a> From<&'a PythonModuleBytecodeFromSource> for PythonResource<'a> {
902    fn from(m: &'a PythonModuleBytecodeFromSource) -> Self {
903        PythonResource::ModuleBytecodeRequest(Cow::Borrowed(m))
904    }
905}
906
907impl<'a> From<PythonModuleBytecode> for PythonResource<'a> {
908    fn from(m: PythonModuleBytecode) -> Self {
909        PythonResource::ModuleBytecode(Cow::Owned(m))
910    }
911}
912
913impl<'a> From<&'a PythonModuleBytecode> for PythonResource<'a> {
914    fn from(m: &'a PythonModuleBytecode) -> Self {
915        PythonResource::ModuleBytecode(Cow::Borrowed(m))
916    }
917}
918
919impl<'a> From<PythonPackageResource> for PythonResource<'a> {
920    fn from(r: PythonPackageResource) -> Self {
921        PythonResource::PackageResource(Cow::Owned(r))
922    }
923}
924
925impl<'a> From<&'a PythonPackageResource> for PythonResource<'a> {
926    fn from(r: &'a PythonPackageResource) -> Self {
927        PythonResource::PackageResource(Cow::Borrowed(r))
928    }
929}
930
931impl<'a> From<PythonPackageDistributionResource> for PythonResource<'a> {
932    fn from(r: PythonPackageDistributionResource) -> Self {
933        PythonResource::PackageDistributionResource(Cow::Owned(r))
934    }
935}
936
937impl<'a> From<&'a PythonPackageDistributionResource> for PythonResource<'a> {
938    fn from(r: &'a PythonPackageDistributionResource) -> Self {
939        PythonResource::PackageDistributionResource(Cow::Borrowed(r))
940    }
941}
942
943impl<'a> From<PythonExtensionModule> for PythonResource<'a> {
944    fn from(r: PythonExtensionModule) -> Self {
945        PythonResource::ExtensionModule(Cow::Owned(r))
946    }
947}
948
949impl<'a> From<&'a PythonExtensionModule> for PythonResource<'a> {
950    fn from(r: &'a PythonExtensionModule) -> Self {
951        PythonResource::ExtensionModule(Cow::Borrowed(r))
952    }
953}
954
955impl<'a> From<PythonEggFile> for PythonResource<'a> {
956    fn from(e: PythonEggFile) -> Self {
957        PythonResource::EggFile(Cow::Owned(e))
958    }
959}
960
961impl<'a> From<&'a PythonEggFile> for PythonResource<'a> {
962    fn from(e: &'a PythonEggFile) -> Self {
963        PythonResource::EggFile(Cow::Borrowed(e))
964    }
965}
966
967impl<'a> From<PythonPathExtension> for PythonResource<'a> {
968    fn from(e: PythonPathExtension) -> Self {
969        PythonResource::PathExtension(Cow::Owned(e))
970    }
971}
972
973impl<'a> From<&'a PythonPathExtension> for PythonResource<'a> {
974    fn from(e: &'a PythonPathExtension) -> Self {
975        PythonResource::PathExtension(Cow::Borrowed(e))
976    }
977}
978
979impl<'a> From<File> for PythonResource<'a> {
980    fn from(f: File) -> Self {
981        PythonResource::File(Cow::Owned(f))
982    }
983}
984
985impl<'a> From<&'a File> for PythonResource<'a> {
986    fn from(f: &'a File) -> Self {
987        PythonResource::File(Cow::Borrowed(f))
988    }
989}
990
991#[cfg(test)]
992mod tests {
993    use super::*;
994
995    const DEFAULT_CACHE_TAG: &str = "cpython-39";
996
997    #[test]
998    fn test_is_in_packages() {
999        let source = PythonResource::ModuleSource(Cow::Owned(PythonModuleSource {
1000            name: "foo".to_string(),
1001            source: FileData::Memory(vec![]),
1002            is_package: false,
1003            cache_tag: DEFAULT_CACHE_TAG.to_string(),
1004            is_stdlib: false,
1005            is_test: false,
1006        }));
1007        assert!(source.is_in_packages(&["foo".to_string()]));
1008        assert!(!source.is_in_packages(&[]));
1009        assert!(!source.is_in_packages(&["bar".to_string()]));
1010
1011        let bytecode = PythonResource::ModuleBytecode(Cow::Owned(PythonModuleBytecode {
1012            name: "foo".to_string(),
1013            bytecode: FileData::Memory(vec![]),
1014            optimize_level: BytecodeOptimizationLevel::Zero,
1015            is_package: false,
1016            cache_tag: DEFAULT_CACHE_TAG.to_string(),
1017            is_stdlib: false,
1018            is_test: false,
1019        }));
1020        assert!(bytecode.is_in_packages(&["foo".to_string()]));
1021        assert!(!bytecode.is_in_packages(&[]));
1022        assert!(!bytecode.is_in_packages(&["bar".to_string()]));
1023    }
1024
1025    #[test]
1026    fn package_distribution_resources_path_normalization() {
1027        // Package names are normalized to lowercase and have hyphens replaced
1028        // by underscores.
1029        let mut r = PythonPackageDistributionResource {
1030            location: PythonPackageDistributionResourceFlavor::DistInfo,
1031            package: "FoO-Bar".into(),
1032            version: "1.0".into(),
1033            name: "resource.txt".into(),
1034            data: vec![42].into(),
1035        };
1036
1037        assert_eq!(
1038            r.resolve_path("prefix"),
1039            PathBuf::from("prefix")
1040                .join("foo_bar-1.0.dist-info")
1041                .join("resource.txt")
1042        );
1043
1044        r.location = PythonPackageDistributionResourceFlavor::EggInfo;
1045
1046        assert_eq!(
1047            r.resolve_path("prefix"),
1048            PathBuf::from("prefix")
1049                .join("foo_bar-1.0.egg-info")
1050                .join("resource.txt")
1051        );
1052    }
1053}