Skip to content

Commit 2205787

Browse files
committed
dropped dependency on attrs
1 parent a46079f commit 2205787

File tree

6 files changed

+109
-76
lines changed

6 files changed

+109
-76
lines changed

fileformats/core/converter.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from abc import ABCMeta
22
import typing as ty
33
import logging
4-
import attrs
54
from .utils import describe_task, matching_source
65
from .exceptions import FileFormatsError
76

@@ -11,15 +10,22 @@
1110
logger = logging.getLogger("fileformats")
1211

1312

14-
@attrs.define
1513
class ConverterWrapper:
1614
"""Wraps a converter task in a workflow so that the in_file and out_file names can
1715
be mapped onto their standardised names, "in_file" and "out_file" if necessary
1816
"""
1917

2018
task_spec: ty.Callable
21-
in_file: ty.Optional[str] = None
22-
out_file: ty.Optional[str] = None
19+
in_file: ty.Optional[str]
20+
out_file: ty.Optional[str]
21+
22+
def __init__(self, task_spec, in_file, out_file):
23+
self.task_spec = task_spec
24+
self.in_file = in_file
25+
self.out_file = out_file
26+
27+
def __repr__(self):
28+
return f"{self.__class__.__name__}({self.task_spec}, {self.in_file}, {self.out_file})"
2329

2430
def __call__(self, name=None, **kwargs):
2531
from pydra.engine import Workflow

fileformats/core/field.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from __future__ import annotations
22
import typing as ty
3-
import attrs
43
from .utils import (
54
classproperty,
65
)
@@ -10,25 +9,36 @@
109
from .datatype import DataType
1110

1211

13-
@attrs.define(repr=False)
1412
class Field(DataType):
15-
value = attrs.field()
13+
"""Base class for all field formats"""
1614

1715
type = None
1816
is_field = True
1917
primitive = None
18+
metadata = None # Empty metadata dict for duck-typing with file-sets
2019

21-
def __str__(self):
20+
def __init__(self, value):
21+
self.value = value
22+
23+
def __eq__(self, field) -> bool:
24+
return (
25+
isinstance(field, Field)
26+
and self.mime_like == field.mime_like
27+
and self.value == field.value
28+
)
29+
30+
def __ne__(self, other) -> bool:
31+
return not self == other
32+
33+
def __hash__(self) -> int:
34+
return hash((self.mime_like, self.value))
35+
36+
def __str__(self) -> str:
2237
return str(self.value)
2338

24-
def __repr__(self):
39+
def __repr__(self) -> str:
2540
return f"{self.type_name}({str(self)})"
2641

27-
@property
28-
def metadata(self):
29-
"""Empty metadata dict for duck-typing with file-sets"""
30-
return {}
31-
3242
@classproperty
3343
def all_fields(cls) -> ty.List[ty.Type[Field]]: # pylint: disable=no-self-argument
3444
"""Iterate over all field formats in fileformats.* namespaces"""

fileformats/core/fileset.py

Lines changed: 65 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
from pathlib import Path
1414
import hashlib
1515
import logging
16-
import attrs
1716
from .utils import (
1817
classproperty,
1918
fspaths_converter,
@@ -50,7 +49,6 @@
5049
logger = logging.getLogger("fileformats")
5150

5251

53-
@attrs.define(repr=False)
5452
class FileSet(DataType):
5553
"""
5654
The base class for all format types within the fileformats package. A generic
@@ -67,55 +65,45 @@ class FileSet(DataType):
6765
extras hook
6866
"""
6967

70-
fspaths: ty.FrozenSet[Path] = attrs.field(default=None, converter=fspaths_converter)
71-
_metadata: ty.Optional[ty.Dict[str, ty.Any]] = attrs.field(
72-
default=False,
73-
eq=False,
74-
order=False,
75-
)
76-
77-
@_metadata.validator
78-
def metadata_validator(self, _, val):
79-
if val and not isinstance(val, dict):
80-
raise TypeError(
81-
f"Fileset metadata value needs to be None or dict, not {val} ({self.fspaths})"
82-
)
83-
84-
# Explicitly set the Internet Assigned Numbers Authority (https://iana_mime.org) MIME
85-
# type to None for any base classes that should not correspond to a MIME or MIME-like
86-
# type.
87-
iana_mime = None
88-
ext = None
89-
alternate_exts = ()
68+
# Class attributes
9069

9170
# Store converters registered by @converter decorator that convert to FileSet
9271
# NB: each class will have its own version of this dictionary
9372
converters = {}
9473

74+
# differentiate between Field and other DataType classes
9575
is_fileset = True
9676

97-
def __hash__(self):
98-
return hash(self.fspaths)
77+
# File extensions associated with file format
78+
ext = None
79+
alternate_exts = ()
9980

100-
def __repr__(self):
101-
return f"{self.type_name}('" + "', '".join(str(p) for p in self.fspaths) + "')"
81+
# to be overridden in subclasses
82+
# Explicitly set the Internet Assigned Numbers Authority (https://iana_mime.org) MIME
83+
# type to None for any base classes that should not correspond to a MIME or MIME-like
84+
# type.
85+
iana_mime = None
10286

103-
def __getitem__(self, name):
104-
return self.metadata[name]
87+
# Member attributes
88+
fspaths: ty.FrozenSet[Path]
89+
_metadata: ty.Union[ty.Dict[str, ty.Any], bool, None]
10590

106-
def __attrs_post_init__(self):
107-
# Check required properties don't raise errors
108-
for prop_name in self.required_properties():
109-
getattr(self, prop_name)
110-
# Loop through all attributes and find methods marked by CHECK_ANNOTATION
111-
for check in self.checks():
112-
getattr(self, check)()
91+
def __init__(self, fspaths, metadata=False):
92+
self._validate_class()
93+
self.fspaths = fspaths_converter(fspaths)
94+
self._validate_fspaths()
95+
self._additional_fspaths()
96+
if metadata and not isinstance(metadata, dict):
97+
raise TypeError(
98+
f"Fileset metadata value needs to be None or dict, not {metadata} ({self})"
99+
)
100+
self._metadata = metadata
101+
self._validate_properties()
113102

114-
@fspaths.validator
115-
def validate_fspaths(self, _, fspaths):
116-
if not fspaths:
103+
def _validate_fspaths(self):
104+
if not self.fspaths:
117105
raise FileFormatsError(f"No file-system paths provided to {self}")
118-
missing = [p for p in fspaths if not p or not p.exists()]
106+
missing = [p for p in self.fspaths if not p or not p.exists()]
119107
if missing:
120108
missing_str = "\n".join(str(p) for p in missing)
121109
msg = (
@@ -134,6 +122,36 @@ def validate_fspaths(self, _, fspaths):
134122
msg += "\n".join(str(p) for p in parent.iterdir())
135123
raise FileNotFoundError(msg)
136124

125+
def _validate_class(self):
126+
"""Check that the class has been correctly defined"""
127+
128+
def _additional_fspaths(self):
129+
"""Additional checks to be performed on the file-system paths provided to the"""
130+
131+
def _validate_properties(self):
132+
# Check required properties don't raise errors
133+
for prop_name in self.required_properties():
134+
getattr(self, prop_name)
135+
# Loop through all attributes and find methods marked by CHECK_ANNOTATION
136+
for check in self.checks():
137+
getattr(self, check)()
138+
139+
def __eq__(self, other) -> bool:
140+
return (
141+
isinstance(other, FileSet)
142+
and self.mime_like == other.mime_like
143+
and self.fspaths == other.fspaths
144+
)
145+
146+
def __ne__(self, other) -> bool:
147+
return not self.__eq__(other)
148+
149+
def __hash__(self) -> int:
150+
return hash((self.mime_like, self.fspaths))
151+
152+
def __repr__(self) -> str:
153+
return f"{self.type_name}('" + "', '".join(str(p) for p in self.fspaths) + "')"
154+
137155
@property
138156
def parent(self) -> Path:
139157
"A common parent directory for all the top-level paths in the file-set"
@@ -1558,20 +1576,21 @@ def copy_to(self, *args, **kwargs):
15581576
_formats_by_name = None
15591577

15601578

1561-
@attrs.define(slots=False, repr=False)
15621579
class MockMixin:
15631580
"""Strips out validation methods of a class, allowing it to be mocked in a way that
15641581
still satisfies type-checking"""
15651582

15661583
# Mirror fspaths here so we can unset its validator
1567-
fspaths: ty.FrozenSet[Path] = attrs.field(default=None, converter=fspaths_converter)
1584+
fspaths: ty.FrozenSet[Path]
1585+
1586+
def _additional_fspaths(self):
1587+
pass # disable implicit addition of related fspaths
15681588

1569-
def __attrs_post_init__(self):
1570-
pass
1589+
def _validate_fspaths(self):
1590+
pass # disable validation of fspaths
15711591

1572-
@fspaths.validator
1573-
def validate_fspaths(self, _, fspaths):
1574-
pass
1592+
def _validate_properties(self):
1593+
pass # disable validation of properties
15751594

15761595
@classproperty
15771596
def type_name(cls):

fileformats/core/hook.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,11 @@
33
import inspect
44
from itertools import zip_longest
55
import functools
6-
import attrs
76
import urllib.error
87
from .datatype import DataType
98
from .converter import ConverterWrapper
109
from .exceptions import FormatConversionError, FileFormatsExtrasError
11-
from .utils import import_extras_module, check_package_exists_on_pypi
10+
from .utils import import_extras_module, check_package_exists_on_pypi, add_exc_note
1211

1312

1413
__all__ = ["required", "check", "converter"]
@@ -79,6 +78,16 @@ def converter(
7978
# "out"
8079
from pydra.engine.helpers import make_klass
8180

81+
try:
82+
import attrs
83+
except ImportError as e:
84+
add_exc_note(
85+
e,
86+
"To use the 'converter' decorator you need to have the 'attrs' package "
87+
"installed, this should be installed with Pydra by default",
88+
)
89+
raise e
90+
8291
def decorator(task_spec):
8392
out_file_local = out_file
8493
if source_format is None or target_format is None:

fileformats/core/mixin.py

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -110,31 +110,21 @@ class WithAdjacentFiles:
110110
different extensions
111111
112112
Note that WithAdjacentFiles must come before the primary type in the method-resolution
113-
order of the class so it can override the '__attrs_post_init__' method in
114-
post_init_super class (typically FileSet), e.g.
113+
order of the class so it can override the '_additional_paths' method in
115114
116115
class MyFileFormatWithSeparateHeader(WithSeparateHeader, MyFileFormat):
117116
118117
header_type = MyHeaderType
119-
120-
Class Attrs
121-
-----------
122-
post_init_super : type
123-
the format class the WithAdjacentFiles mixin is mixed with that defines the
124-
__attrs_post_init__ method that should be called once the adjacent files
125-
are added to the self.fspaths attribute to run checks.
126118
"""
127119

128-
post_init_super = FileSet
129120
fspaths: ty.FrozenSet[Path]
130121

131-
def __attrs_post_init__(self):
122+
def _additional_fspaths(self):
132123
if len(self.fspaths) == 1:
133124
self.fspaths |= self.get_adjacent_files()
134125
trim = True
135126
else:
136127
trim = False
137-
self.post_init_super.__attrs_post_init__(self)
138128
if trim:
139129
self.trim_paths()
140130

@@ -285,7 +275,7 @@ def my_func(file: MyFormatWithClassifiers[Integer]):
285275
ordered_classifiers = False
286276
generically_classifies = False
287277

288-
def __attrs_pre_init__(self):
278+
def _validate_class(self):
289279
if self.wildcard_classifiers():
290280
raise FileFormatsError(
291281
f"Can instantiate {type(self)} class as it has wildcard classifiers "

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ description = "Classes for representing different file formats in Python classes
88
readme = "README.rst"
99
requires-python = ">=3.8"
1010
dependencies = [
11-
"attrs>=22.1.0",
1211
"typing_extensions >=4.6.3; python_version < '3.11'"
1312
]
1413
license = {file = "LICENSE"}

0 commit comments

Comments
 (0)