From 1c94bc22e71b9fa494bdb10c8f6200db83c84975 Mon Sep 17 00:00:00 2001 From: Tarun Pratap Singh <101409098+Wackyator@users.noreply.github.com> Date: Mon, 26 Feb 2024 20:44:58 +0530 Subject: [PATCH] feat: Add AboutJson (#544) --- py-rattler/docs/about_json.md | 4 + py-rattler/mkdocs.yml | 2 + py-rattler/rattler/__init__.py | 3 +- py-rattler/rattler/package/__init__.py | 3 +- py-rattler/rattler/package/about_json.py | 268 +++++++++++++++++++++++ py-rattler/src/about_json.rs | 144 ++++++++++++ py-rattler/src/lib.rs | 4 + test-data/dummy-about.json | 12 + 8 files changed, 438 insertions(+), 2 deletions(-) create mode 100644 py-rattler/docs/about_json.md create mode 100644 py-rattler/rattler/package/about_json.py create mode 100644 py-rattler/src/about_json.rs create mode 100644 test-data/dummy-about.json diff --git a/py-rattler/docs/about_json.md b/py-rattler/docs/about_json.md new file mode 100644 index 000000000..12d6cb3a6 --- /dev/null +++ b/py-rattler/docs/about_json.md @@ -0,0 +1,4 @@ +# AboutJson + +::: rattler.package.about_json + diff --git a/py-rattler/mkdocs.yml b/py-rattler/mkdocs.yml index eab37e433..d7a9ca1c3 100644 --- a/py-rattler/mkdocs.yml +++ b/py-rattler/mkdocs.yml @@ -39,6 +39,8 @@ nav: - channel: - ChannelConfig: channel_config.md - Channel: channel.md + - metadata: + - AboutJson: about_json.md - match_spec: - MatchSpec: match_spec.md - NamelessMatchSpec: nameless_match_spec.md diff --git a/py-rattler/rattler/__init__.py b/py-rattler/rattler/__init__.py index d3af7085c..6a76ebaf3 100644 --- a/py-rattler/rattler/__init__.py +++ b/py-rattler/rattler/__init__.py @@ -10,7 +10,7 @@ from rattler.channel import Channel, ChannelConfig from rattler.networking import AuthenticatedClient, fetch_repo_data from rattler.virtual_package import GenericVirtualPackage, VirtualPackage -from rattler.package import PackageName +from rattler.package import PackageName, AboutJson from rattler.prefix import PrefixRecord, PrefixPaths from rattler.solver import solve from rattler.platform import Platform @@ -60,4 +60,5 @@ "Platform", "link", "index", + "AboutJson", ] diff --git a/py-rattler/rattler/package/__init__.py b/py-rattler/rattler/package/__init__.py index c26d6b43e..269c235fd 100644 --- a/py-rattler/rattler/package/__init__.py +++ b/py-rattler/rattler/package/__init__.py @@ -1,3 +1,4 @@ from rattler.package.package_name import PackageName +from rattler.package.about_json import AboutJson -__all__ = ["PackageName"] +__all__ = ["PackageName", "AboutJson"] diff --git a/py-rattler/rattler/package/about_json.py b/py-rattler/rattler/package/about_json.py new file mode 100644 index 000000000..dd4ee4e7f --- /dev/null +++ b/py-rattler/rattler/package/about_json.py @@ -0,0 +1,268 @@ +from __future__ import annotations + +import os +from pathlib import Path +from typing import List, Optional + +from rattler.rattler import PyAboutJson + + +class AboutJson: + """ + The `about.json` file contains metadata about the package. + """ + + _inner: PyAboutJson + + @staticmethod + def from_path(path: os.PathLike[str]) -> AboutJson: + """ + Parses the object from a file specified by a `path`, using a format + appropriate for the file type. + + For example, if the file is in JSON format, this function reads the data + from the file at the specified path, parse the JSON string and return the + resulting object. If the file is not in a parsable format or if the file + could not read, this function returns an error. + + Examples + -------- + ```python + >>> about = AboutJson.from_path("../test-data/dummy-about.json") + >>> about + AboutJson() + >>> + ``` + """ + return AboutJson._from_py_about_json(PyAboutJson.from_path(Path(path))) + + @staticmethod + def from_package_directory(path: os.PathLike[str]) -> AboutJson: + """ + Parses the object by looking up the appropriate file from the root of the + specified Conda archive directory, using a format appropriate for the file + type. + + For example, if the file is in JSON format, this function reads the + appropriate file from the archive, parse the JSON string and return the + resulting object. If the file is not in a parsable format or if the file + could not be read, this function returns an error. + """ + return AboutJson._from_py_about_json( + PyAboutJson.from_package_directory(Path(path)) + ) + + @staticmethod + def from_str(string: str) -> AboutJson: + """ + Parses the object from a string, using a format appropriate for the file + type. + + For example, if the file is in JSON format, this function parses the JSON + string and returns the resulting object. If the file is not in a parsable + format, this function returns an error. + + Examples + -------- + ```python + >>> import json + >>> with open("../test-data/dummy-about.json", 'r') as file: + ... json_str = json.dumps(json.load(file)) + >>> about = AboutJson.from_str(json_str) + >>> about + AboutJson() + >>> + ``` + """ + return AboutJson._from_py_about_json(PyAboutJson.from_str(string)) + + @staticmethod + def package_path() -> str: + """ + Returns the path to the file within the Conda archive. + + The path is relative to the root of the archive and includes any necessary + directories. + + Examples + -------- + ```python + >>> AboutJson.package_path() + 'info/about.json' + >>> + ``` + """ + return PyAboutJson.package_path() + + @property + def channels(self) -> List[str]: + """ + A list of channels that where used during the build. + + Examples + -------- + ```python + >>> about = AboutJson.from_path("../test-data/dummy-about.json") + >>> about.channels + ['https://conda.anaconda.org/conda-forge'] + >>> + ``` + """ + return self._inner.channels + + @property + def description(self) -> Optional[str]: + """ + Description of the package. + + Examples + -------- + ```python + >>> about = AboutJson.from_path("../test-data/dummy-about.json") + >>> about.description + 'A dummy description.' + >>> + ``` + """ + if description := self._inner.description: + return description + + return None + + @property + def dev_url(self) -> List[str]: + """ + A list of URLs to the development page of the package. + + Examples + -------- + ```python + >>> about = AboutJson.from_path("../test-data/dummy-about.json") + >>> about.dev_url + ['https://github.com/mamba-org/rattler'] + >>> + ``` + """ + return self._inner.dev_url + + @property + def doc_url(self) -> List[str]: + """ + A list of URLs to the documentation of the package. + + Examples + -------- + ```python + >>> about = AboutJson.from_path("../test-data/dummy-about.json") + >>> about.doc_url + ['https://mamba-org.github.io/rattler/py-rattler/'] + >>> + ``` + """ + return self._inner.doc_url + + @property + def home(self) -> List[str]: + """ + A list URL to the homepage of the package. + + Examples + -------- + ```python + >>> about = AboutJson.from_path("../test-data/dummy-about.json") + >>> about.home + ['http://github.com/mamba-org/rattler'] + >>> + ``` + """ + return self._inner.home + + @property + def license(self) -> Optional[str]: + """ + The license of the package. + + Examples + -------- + ```python + >>> about = AboutJson.from_path("../test-data/dummy-about.json") + >>> about.license + 'BSD-3-Clause' + >>> + ``` + """ + if license := self._inner.license: + return license + + return None + + @property + def license_family(self) -> Optional[str]: + """ + The license family of the package. + + Examples + -------- + ```python + >>> about = AboutJson.from_path("../test-data/dummy-about.json") + >>> about.license_family + >>> type(about.license_family) + + >>> + ``` + """ + if license_family := self._inner.license_family: + return license_family + + return None + + @property + def source_url(self) -> Optional[str]: + """ + The URL to the latest source code of the package. + + Examples + -------- + ```python + >>> about = AboutJson.from_path("../test-data/dummy-about.json") + >>> about.source_url + 'https://github.com/mamba-org/rattler' + >>> + ``` + """ + if source_url := self._inner.source_url: + return source_url + + return None + + @property + def summary(self) -> Optional[str]: + """ + A Short summary description. + + Examples + -------- + ```python + >>> about = AboutJson.from_path("../test-data/dummy-about.json") + >>> about.summary + 'A dummy summary.' + >>> + ``` + """ + if summary := self._inner.summary: + return summary + + return None + + @classmethod + def _from_py_about_json(cls, py_about_json: PyAboutJson) -> AboutJson: + about_json = cls.__new__(cls) + about_json._inner = py_about_json + + return about_json + + def __repr__(self) -> str: + """ + Returns a representation of the AboutJson. + """ + return "AboutJson()" diff --git a/py-rattler/src/about_json.rs b/py-rattler/src/about_json.rs new file mode 100644 index 000000000..54709ba1c --- /dev/null +++ b/py-rattler/src/about_json.rs @@ -0,0 +1,144 @@ +use std::path::PathBuf; + +use pyo3::{pyclass, pymethods, PyResult}; +use rattler_conda_types::package::{AboutJson, PackageFile}; + +use crate::error::PyRattlerError; + +/// The `about.json` file contains metadata about the package +#[pyclass] +#[repr(transparent)] +#[derive(Clone)] +pub struct PyAboutJson { + pub(crate) inner: AboutJson, +} + +impl From for PyAboutJson { + fn from(value: AboutJson) -> Self { + Self { inner: value } + } +} + +impl From for AboutJson { + fn from(value: PyAboutJson) -> Self { + value.inner + } +} + +#[pymethods] +impl PyAboutJson { + /// Parses the object from a file specified by a `path`, using a format appropriate for the file + /// type. + /// + /// For example, if the file is in JSON format, this function reads the data from the file at + /// the specified path, parse the JSON string and return the resulting object. If the file is + /// not in a parsable format or if the file could not read, this function returns an error. + #[staticmethod] + pub fn from_path(path: PathBuf) -> PyResult { + Ok(AboutJson::from_path(path) + .map(Into::into) + .map_err(PyRattlerError::from)?) + } + + /// Parses the object by looking up the appropriate file from the root of the specified Conda + /// archive directory, using a format appropriate for the file type. + /// + /// For example, if the file is in JSON format, this function reads the appropriate file from + /// the archive, parse the JSON string and return the resulting object. If the file is not in a + /// parsable format or if the file could not be read, this function returns an error. + #[staticmethod] + pub fn from_package_directory(path: PathBuf) -> PyResult { + Ok(AboutJson::from_package_directory(path) + .map(Into::into) + .map_err(PyRattlerError::from)?) + } + + /// Parses the object from a string, using a format appropriate for the file type. + /// + /// For example, if the file is in JSON format, this function parses the JSON string and returns + /// the resulting object. If the file is not in a parsable format, this function returns an + /// error. + #[staticmethod] + pub fn from_str(str: &str) -> PyResult { + Ok(AboutJson::from_str(str) + .map(Into::into) + .map_err(PyRattlerError::from)?) + } + + /// Returns the path to the file within the Conda archive. + /// + /// The path is relative to the root of the archive and include any necessary directories. + #[staticmethod] + pub fn package_path() -> PathBuf { + AboutJson::package_path().to_owned() + } + + /// A list of channels that where used during the build + #[getter] + pub fn channels(&self) -> Vec { + self.inner.channels.clone() + } + + /// Description of the package + #[getter] + pub fn description(&self) -> Option { + self.inner.description.clone() + } + + /// URL to the development page of the package + #[getter] + pub fn dev_url(&self) -> Vec { + self.inner + .dev_url + .clone() + .into_iter() + .map(|url| url.to_string()) + .collect() + } + + /// URL to the documentation of the package + #[getter] + pub fn doc_url(&self) -> Vec { + self.inner + .doc_url + .clone() + .into_iter() + .map(|url| url.to_string()) + .collect() + } + + /// URL to the homepage of the package + #[getter] + pub fn home(&self) -> Vec { + self.inner + .home + .clone() + .into_iter() + .map(|url| url.to_string()) + .collect() + } + + /// Optionally, the license + #[getter] + pub fn license(&self) -> Option { + self.inner.license.clone() + } + + /// Optionally, the license family + #[getter] + pub fn license_family(&self) -> Option { + self.inner.license_family.clone() + } + + /// URL to the latest source code of the package + #[getter] + pub fn source_url(&self) -> Option { + self.inner.source_url.clone().map(|v| v.to_string()) + } + + /// Short summary description + #[getter] + pub fn summary(&self) -> Option { + self.inner.summary.clone() + } +} diff --git a/py-rattler/src/lib.rs b/py-rattler/src/lib.rs index a7ab18f2a..15d548c64 100644 --- a/py-rattler/src/lib.rs +++ b/py-rattler/src/lib.rs @@ -1,3 +1,4 @@ +mod about_json; mod channel; mod error; mod generic_virtual_package; @@ -18,6 +19,7 @@ mod solver; mod version; mod virtual_package; +use about_json::PyAboutJson; use channel::{PyChannel, PyChannelConfig}; use error::{ ActivationException, CacheDirException, ConvertSubdirException, DetectVirtualPackageException, @@ -92,6 +94,8 @@ fn rattler(py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::().unwrap(); m.add_class::().unwrap(); + m.add_class::().unwrap(); + m.add_function(wrap_pyfunction!(py_solve, m).unwrap()) .unwrap(); m.add_function(wrap_pyfunction!(get_rattler_version, m).unwrap()) diff --git a/test-data/dummy-about.json b/test-data/dummy-about.json new file mode 100644 index 000000000..5bfebaa35 --- /dev/null +++ b/test-data/dummy-about.json @@ -0,0 +1,12 @@ +{ + "channels": [ + "https://conda.anaconda.org/conda-forge" + ], + "description": "A dummy description.", + "dev_url": "https://github.com/mamba-org/rattler", + "doc_url": "https://mamba-org.github.io/rattler/py-rattler/", + "home": "http://github.com/mamba-org/rattler", + "license": "BSD-3-Clause", + "source_url": "https://github.com/mamba-org/rattler", + "summary": "A dummy summary." +}