-
Notifications
You must be signed in to change notification settings - Fork 85
Pydantic integration and AnyPath polymorphic class #130
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
331aae2
Add AnyPath polymorphic virtual superclass.
jayqi 51da9d5
Refactor requirements to parse from single file
jayqi 91b64ed
Add pydantic as a dev requirement
jayqi e30b0ab
Ignore mypy complaints. We're going to live dangerously
jayqi bf2bf66
Docs
jayqi c96b35d
Fix typo
jayqi 44ff9ab
Add more docs for anypath
jayqi 3d542de
Fix typo
jayqi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,3 @@ | ||
| include README.md | ||
| include LICENSE | ||
| include requirements/*.txt | ||
| include requirements.txt |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| from pathlib import Path | ||
| from typing import Union | ||
|
|
||
| from .cloudpath import InvalidPrefix, CloudPath | ||
|
|
||
|
|
||
| class AnyPathTypeError(TypeError): | ||
| pass | ||
|
|
||
|
|
||
| class AnyPathMeta(type): | ||
| """Metaclass for AnyPath that implements special methods so that AnyPath works as a virtual | ||
| superclass when using isinstance or issubclass checks on CloudPath or Path inputs. See | ||
| [PEP 3119](https://www.python.org/dev/peps/pep-3119/#overloading-isinstance-and-issubclass).""" | ||
|
|
||
| def __instancecheck__(cls, inst): | ||
| return isinstance(inst, CloudPath) or isinstance(inst, Path) | ||
|
|
||
| def __subclasscheck__(cls, sub): | ||
| return issubclass(sub, CloudPath) or issubclass(sub, Path) | ||
|
|
||
|
|
||
| class AnyPath(metaclass=AnyPathMeta): | ||
| """Polymorphic virtual superclass for CloudPath and pathlib.Path. Constructing an instance will | ||
| automatically dispatch to CloudPath or Path based on the input. It also supports both | ||
| isinstance and issubclass checks. | ||
|
|
||
| This class also integrates with Pydantic. When used as a type declaration for a Pydantic | ||
| BaseModel, the Pydantic validation process will appropriately run inputs through this class' | ||
| constructor and dispatch to CloudPath or Path. | ||
| """ | ||
|
|
||
| def __new__(cls, *args, **kwargs) -> Union[CloudPath, Path]: # type: ignore | ||
| try: | ||
| return CloudPath(*args, **kwargs) # type: ignore | ||
| except InvalidPrefix as cloudpath_exception: | ||
| try: | ||
| return Path(*args, **kwargs) | ||
| except TypeError as path_exception: | ||
| raise AnyPathTypeError( | ||
| " ".join( | ||
| [ | ||
| "Invalid input for both CloudPath and Path.", | ||
| f"CloudPath exception: {repr(cloudpath_exception)}", | ||
| f"Path exception: {repr(path_exception)}", | ||
| ] | ||
| ) | ||
| ) | ||
|
|
||
| @classmethod | ||
| def __get_validators__(cls): | ||
| """Pydantic special method. See | ||
| https://pydantic-docs.helpmanual.io/usage/types/#custom-data-types""" | ||
| yield cls._validate | ||
|
|
||
| @classmethod | ||
| def _validate(cls, value) -> Union[CloudPath, Path]: | ||
| """Used as a Pydantic validator. See | ||
| https://pydantic-docs.helpmanual.io/usage/types/#custom-data-types""" | ||
| # Note __new__ is static method and not a class method | ||
| return cls.__new__(cls, value) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| # AnyPath Polymorphic Class | ||
|
|
||
| `cloudpathlib` implements a special `AnyPath` polymorphic class. This class will automatically instantiate a cloud path instance or a `pathlib.Path` instance appropriately from your input. It's also a virtual superclass of `CloudPath` and `Path`, so `isinstance` and `issubclass` checks will work in the expected way. | ||
|
|
||
| This functionality can be handy for situations when you want to support both local filepaths and cloud storage filepaths. If you use `AnyPath`, your code can switch between them seamlessly based on the contents of provided filepaths with needing any `if`-`else` conditional blocks. | ||
|
|
||
| ## Example | ||
|
|
||
| ```python | ||
| from cloudpathlib import AnyPath | ||
|
|
||
| path = AnyPath("mydir/myfile.txt") | ||
| path | ||
| #> PosixPath('mydir/myfile.txt') | ||
|
|
||
| cloud_path = AnyPath("s3://mybucket/myfile.txt") | ||
| cloud_path | ||
| #> S3Path('s3://mybucket/myfile.txt') | ||
|
|
||
| isinstance(path, AnyPath) | ||
| #> True | ||
| isinstance(cloud_path, AnyPath) | ||
| #> True | ||
| ``` | ||
|
|
||
| ## How It Works | ||
|
|
||
| The constructor for `AnyPath` will first attempt to run the input through the `CloudPath` base class' constructor, which will validate the input against registered concrete `CloudPath` implementations. This will accept inputs that are already a cloud path class or a string with the appropriate URI scheme prefix (e.g., `s3://`). If no implementation validates successfully, it will then try to run the input through the `Path` constructor. If the `Path` constructor fails and raises a `TypeError`, then the `AnyPath` constructor will raise an `AnyPathTypeError` exception. | ||
|
|
||
| The virtual superclass functionality with `isinstance` and `issubclass` with the `__instancecheck__` and `__subclasscheck__` special methods per [PEP 3119](https://www.python.org/dev/peps/pep-3119/#overloading-isinstance-and-issubclass)'s specification. | ||
|
|
||
| --- | ||
| <sup>Examples created with [reprexlite](https://github.com/jayqi/reprexlite)</sup> | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| # AnyPath | ||
|
|
||
| ::: cloudpathlib.anypath.AnyPath |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| # Integrations with Other Libraries | ||
|
|
||
| ## Pydantic | ||
|
|
||
| `cloudpathlib` integrates with [Pydantic](https://pydantic-docs.helpmanual.io/)'s data validation. You can declare fields with cloud path classes, and Pydantic's validation mechanisms will run inputs through the cloud path's constructor. | ||
|
|
||
| ```python | ||
| from cloudpathlib import S3Path | ||
| from pydantic import BaseModel | ||
|
|
||
| class MyModel(BaseModel): | ||
| s3_file: S3Path | ||
|
|
||
| inst = MyModel(s3_file="s3://mybucket/myfile.txt") | ||
| inst.s3_file | ||
| #> S3Path('s3://mybucket/myfile.txt') | ||
| ``` | ||
|
|
||
| This also works with the `AnyPath` polymorphic class. Inputs will get dispatched and instantiated as the appropriate class. | ||
|
|
||
| ```python | ||
| from cloudpathlib import AnyPath | ||
| from pydantic import BaseModel | ||
|
|
||
| class FancyModel(BaseModel): | ||
| path: AnyPath | ||
|
|
||
| fancy1 = FancyModel(path="s3://mybucket/myfile.txt") | ||
| fancy1.path | ||
| #> S3Path('s3://mybucket/myfile.txt') | ||
|
|
||
| fancy2 = FancyModel(path="mydir/myfile.txt") | ||
| fancy2.path | ||
| #> PosixPath('mydir/myfile.txt') | ||
| ``` | ||
|
|
||
| --- | ||
| <sup>Examples created with [reprexlite](https://github.com/jayqi/reprexlite)</sup> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,7 @@ mkdocstrings | |
| mypy | ||
| pandas | ||
| pillow | ||
| pydantic | ||
| pytest | ||
| pytest-cases | ||
| pytest-cov | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| # Base requirements for library | ||
| importlib_metadata ; python_version < "3.8" | ||
|
|
||
| ## extras: azure | ||
| azure-storage-blob>=12 | ||
|
|
||
| ## extras: gs | ||
| google-cloud-storage | ||
|
|
||
| ## extras: s3 | ||
| boto3 |
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| from pathlib import Path, PosixPath, WindowsPath | ||
|
|
||
| import pytest | ||
|
|
||
| from cloudpathlib.anypath import AnyPath, AnyPathTypeError | ||
| from cloudpathlib.cloudpath import CloudPath | ||
|
|
||
|
|
||
| def test_anypath_path(): | ||
| path = Path("a/b/c") | ||
| assert AnyPath(path) == path | ||
| assert AnyPath(str(path)) == path | ||
|
|
||
| assert isinstance(path, AnyPath) | ||
| assert not isinstance(str(path), AnyPath) | ||
|
|
||
| assert issubclass(Path, AnyPath) | ||
| assert issubclass(PosixPath, AnyPath) | ||
| assert issubclass(WindowsPath, AnyPath) | ||
| assert not issubclass(str, AnyPath) | ||
|
|
||
|
|
||
| def test_anypath_cloudpath(rig): | ||
| cloudpath = rig.create_cloud_path("a/b/c") | ||
| assert AnyPath(cloudpath) == cloudpath | ||
| assert AnyPath(str(cloudpath)) == cloudpath | ||
|
|
||
| assert isinstance(cloudpath, AnyPath) | ||
| assert not isinstance(str(cloudpath), AnyPath) | ||
|
|
||
| assert issubclass(CloudPath, AnyPath) | ||
| assert issubclass(rig.path_class, AnyPath) | ||
|
|
||
|
|
||
| def test_anypath_bad_input(): | ||
| with pytest.raises(AnyPathTypeError): | ||
| AnyPath(0) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| from pathlib import Path | ||
|
|
||
| from pydantic import BaseModel, ValidationError | ||
| import pytest | ||
|
|
||
| from cloudpathlib.anypath import AnyPath | ||
|
|
||
|
|
||
| def test_pydantic_cloudpath(rig): | ||
| class PydanticModel(BaseModel): | ||
| cloud_path: rig.path_class | ||
|
|
||
| cp = rig.create_cloud_path("a/b/c") | ||
|
|
||
| obj = PydanticModel(cloud_path=cp) | ||
| assert obj.cloud_path == cp | ||
|
|
||
| obj = PydanticModel(cloud_path=str(cp)) | ||
| assert obj.cloud_path == cp | ||
|
|
||
| with pytest.raises(ValidationError): | ||
| _ = PydanticModel(cloud_path=0) | ||
|
|
||
|
|
||
| def test_pydantic_anypath(rig): | ||
| class PydanticModel(BaseModel): | ||
| any_path: AnyPath | ||
|
|
||
| cp = rig.create_cloud_path("a/b/c") | ||
|
|
||
| obj = PydanticModel(any_path=cp) | ||
| assert obj.any_path == cp | ||
|
|
||
| obj = PydanticModel(any_path=str(cp)) | ||
| assert obj.any_path == cp | ||
|
|
||
| obj = PydanticModel(any_path=Path("a/b/c")) | ||
| assert obj.any_path == Path("a/b/c") | ||
|
|
||
| obj = PydanticModel(any_path="a/b/c") | ||
| assert obj.any_path == Path("a/b/c") | ||
|
|
||
| with pytest.raises(ValidationError): | ||
| obj = PydanticModel(any_path=0) |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.