From cbe60179528d3c122c99071f8e9faa06e5c765b3 Mon Sep 17 00:00:00 2001 From: Philippe Pinard Date: Sat, 9 Mar 2019 19:29:40 +0000 Subject: [PATCH 1/5] Replace metaclass of descriptors and properties with dataclasses --- pyxray/cbook.py | 153 ------------ pyxray/descriptor.py | 274 +++++++++++---------- pyxray/property.py | 269 ++++++++++---------- pyxray/test_cbook.py | 208 ++-------------- pyxray/test_descriptor.py | 500 ++++++++++++++++++++++++-------------- pyxray/test_property.py | 68 ++---- 6 files changed, 636 insertions(+), 836 deletions(-) diff --git a/pyxray/cbook.py b/pyxray/cbook.py index 0782416..79d7708 100644 --- a/pyxray/cbook.py +++ b/pyxray/cbook.py @@ -3,7 +3,6 @@ """ # Standard library modules. -import weakref # Third party modules. @@ -11,158 +10,6 @@ # Globals and constants variables. -class Immutable(type): - """ - Make a class immutable. - The attribute cannot be added or modified. - The attributes should be defined by the argument ``attrs`` in the class - definition:: - - class Foo(metaclass=Immutable, attrs=('bar',)): - pass - - The constructor of the class then takes the same number of arguments:: - - foo = Foo('abc') - foo.bar # returns 'abc' - """ - - def __new__(cls, name, bases, methods, *, attrs=None): - # Make a __new__ function and add to the class dict - def __new__(cls, *args, **kwargs): - args = list(args) - - for key, value in kwargs.items(): - try: - index = attrs.index(key) - except ValueError: - raise TypeError('Unknown argument: {0}'.format(key)) - args.insert(index, value) - - if len(args) != len(attrs): - raise TypeError('Expected {} arguments'.format(len(attrs))) - - obj = object.__new__(cls) - obj._values = tuple(args) - obj._attrs = tuple(attrs) - - return obj - methods['__new__'] = __new__ - - # Configure __slots__ - methods['__slots__'] = ('__weakref__', '_values', '_attrs') - - # Configure __hash__ - methods['__hash__'] = lambda s: hash((s.__class__, s._values)) - - # Configure copy - methods['__copy__'] = lambda s: s - methods['__deepcopy__'] = lambda s, memo: s - - # Configure pickle - methods['__getnewargs_ex__'] = lambda s: (s._values, {}) - - # Populate a dictionary of field property accessors - methods.update({name: property(lambda s, n=n: s._values[n]) - for n, name in enumerate(attrs)}) - - cls = super().__new__(cls, name, bases, methods) - - return cls - - def __init__(self, name, bases, methods, *, attrs=None): - super().__init__(name, bases, methods) - -class Cachable(type): - """ - From Beazley, D. & Jones, B. K. (2013) Python Cookbook, O'Reilly. - - Creates a cache using the arguments of :meth:`__init__`. - """ - - def __init__(self, name, bases, methods): - super().__init__(name, bases, methods) - self.__cache = weakref.WeakValueDictionary() - - def __call__(self, *args, **kwargs): - cachekey = args + tuple(sorted(kwargs.items())) - if cachekey in self.__cache: - return self.__cache[cachekey] - - # Create object - obj = super().__call__(*args, **kwargs) - - # Cached - self.__cache[cachekey] = obj - - return obj - -class Validable(type): - """ - Validates the object before it is created using the arguments from the - :meth:`__init__`. - The class method :meth:`validate` should be overwritten. - There are three options for the return values of the :meth:`validate`: - - 1. return ``None`` or no return at all: the same arguments and - keyword arguments are passed to the constructor - 2. return a :class:`tuple` and a :class:`dict`: the tuple contains - updated arguments and the dict, updated keyword-arguments - 3. return a :class:`tuple`: the tuple contains updated arguments. - No keyword-arguments is passed to the constructor. - - In all other cases, a :exc:`ValueError` is raised. - """ - - def validate(cls, *args, **kwargs): #@NoSelf #pragma: no cover - return args - - def __call__(self, *args, **kwargs): - out = self.validate(*args, **kwargs) - if out is not None: - if len(out) == 2 and \ - isinstance(out[0], tuple) and \ - isinstance(out[1], dict): - args, kwargs = out - elif isinstance(out, tuple): - args = out - kwargs = {} - else: #pragma: no cover - raise ValueError('Unknown type of return arguments') - - return super().__call__(*args, **kwargs) - -class Reprable(type): - """ - Construct the __repr__ of an object based on the :meth:`_repr_inner` method - if it is defined. - """ - - def __new__(cls, name, bases, methods, *args, **kwargs): - def __repr__(self): - s = '{0}('.format(self.__class__.__name__) - - if hasattr(self, '_repr_inner'): - s += self._repr_inner() - - else: - inner = [] - for attr, value in zip(self._attrs, self._values): - if hasattr(value, '_repr_inner'): - inner.append(value._repr_inner()) - elif '_' in attr: - _, unit = attr.rsplit('_', 1) - inner.append('{0:.4e}{1}'.format(value, unit)) - else: - inner.append('{0}={1}'.format(attr, value)) - s += ', '.join(inner) - - s += ')' - return s - methods['__repr__'] = __repr__ - - return super().__new__(cls, name, bases, methods, *args, **kwargs) - class ProgressMixin: def update(self, progress): diff --git a/pyxray/descriptor.py b/pyxray/descriptor.py index 84a03e8..beb5829 100644 --- a/pyxray/descriptor.py +++ b/pyxray/descriptor.py @@ -6,82 +6,73 @@ 'XrayTransitionSet', 'XrayLine', 'Language', 'Notation', 'Reference'] # Standard library modules. +import dataclasses +import typing # Third party modules. # Local modules. -from pyxray.cbook import Immutable, Cachable, Validable, Reprable # Globals and constants variables. -class _Descriptor(Immutable, Validable, Cachable, Reprable): - pass - -class Element(metaclass=_Descriptor, - attrs=('atomic_number',)): - - @classmethod - def validate(cls, atomic_number): - if atomic_number < 1 or atomic_number > 118: +@dataclasses.dataclass(frozen=True) +class Element: + atomic_number: int + + def __post_init__(self): + if self.atomic_number < 1 or self.atomic_number > 118: raise ValueError('Atomic number ({0}) must be [1, 118]' - .format(atomic_number)) + .format(self.atomic_number)) - def _repr_inner(self): - return 'z={0}'.format(self.z) + def __repr__(self): + return '{}(z={})'.format(self.__class__.__name__, self.atomic_number) @property def z(self): return self.atomic_number -class AtomicShell(metaclass=_Descriptor, - attrs=('principal_quantum_number',)): - - @classmethod - def validate(cls, principal_quantum_number): - if principal_quantum_number < 1: +@dataclasses.dataclass(frozen=True) +class AtomicShell: + principal_quantum_number: int + + def __post_init__(self): + if self.principal_quantum_number < 1: raise ValueError('Principal quantum number ({0}) must be [1, inf[' - .format(principal_quantum_number)) + .format(self.principal_quantum_number)) - def _repr_inner(self): - return 'n={0}'.format(self.n) + def __repr__(self): + return '{}(n={})'.format(self.__class__.__name__, self.principal_quantum_number) @property def n(self): return self.principal_quantum_number -class AtomicSubshell(metaclass=_Descriptor, - attrs=('atomic_shell', - 'azimuthal_quantum_number', - 'total_angular_momentum_nominator')): +@dataclasses.dataclass(frozen=True) +class AtomicSubshell: + atomic_shell: AtomicShell + azimuthal_quantum_number: int + total_angular_momentum_nominator: int - @classmethod - def validate(cls, - atomic_shell, - azimuthal_quantum_number, - total_angular_momentum_nominator): - if not isinstance(atomic_shell, AtomicShell): - atomic_shell = AtomicShell(atomic_shell) + def __post_init__(self): + if not isinstance(self.atomic_shell, AtomicShell): + object.__setattr__(self, 'atomic_shell', AtomicShell(self.atomic_shell)) lmin = 0 - lmax = atomic_shell.principal_quantum_number - 1 - jmin_n = 2 * abs(azimuthal_quantum_number - 0.5) - jmax_n = 2 * abs(azimuthal_quantum_number + 0.5) + lmax = self.atomic_shell.principal_quantum_number - 1 + jmin_n = 2 * abs(self.azimuthal_quantum_number - 0.5) + jmax_n = 2 * abs(self.azimuthal_quantum_number + 0.5) - if azimuthal_quantum_number < lmin or \ - azimuthal_quantum_number > lmax: + if self.azimuthal_quantum_number < lmin or \ + self.azimuthal_quantum_number > lmax: raise ValueError('Azimuthal quantum number ({0}) must be between [{1}, {2}]' - .format(azimuthal_quantum_number, lmin, lmax)) - if total_angular_momentum_nominator < jmin_n or \ - total_angular_momentum_nominator > jmax_n: + .format(self.azimuthal_quantum_number, lmin, lmax)) + if self.total_angular_momentum_nominator < jmin_n or \ + self.total_angular_momentum_nominator > jmax_n: raise ValueError('Total angular momentum ({0}) must be between [{1}, {2}]' - .format(total_angular_momentum_nominator, jmin_n, jmax_n)) - - return (atomic_shell, - azimuthal_quantum_number, - total_angular_momentum_nominator) - - def _repr_inner(self): - return 'n={0}, l={1}, j={2:.1f}'.format(self.n, self.l, self.j) + .format(self.total_angular_momentum_nominator, jmin_n, jmax_n)) + + def __repr__(self): + return '{}(n={}, l={}, j={:.1f})'.format(self.__class__.__name__, self.n, self.l, self.j) @property def principal_quantum_number(self): @@ -107,9 +98,17 @@ def total_angular_momentum(self): def j(self): return self.total_angular_momentum -class XrayTransition(metaclass=_Descriptor, - attrs=('source_subshell', - 'destination_subshell')): +@dataclasses.dataclass(frozen=True) +class XrayTransition: + source_subshell: AtomicSubshell + destination_subshell: AtomicSubshell + + def __post_init__(self): + if not isinstance(self.source_subshell, AtomicSubshell): + object.__setattr__(self, 'source_subshell', AtomicSubshell(*self.source_subshell)) + + if not isinstance(self.destination_subshell, AtomicSubshell): + object.__setattr__(self, 'destination_subshell', AtomicSubshell(*self.destination_subshell)) @classmethod def is_radiative(cls, source_subshell, destination_subshell): @@ -150,48 +149,47 @@ def electric_quadrupole_permitted(n0, l0, j0_n, n1, l1, j1_n): return True - @classmethod - def validate(cls, source_subshell, destination_subshell): - if not isinstance(source_subshell, AtomicSubshell): - source_subshell = AtomicSubshell(source_subshell) - if not isinstance(destination_subshell, AtomicSubshell): - destination_subshell = AtomicSubshell(destination_subshell) - - return (source_subshell, destination_subshell) - - def _repr_inner(self): - r = '[n={src.n}, l={src.l}, j={src.j:.1f}]' - r += ' -> [n={dest.n}, l={dest.l}, j={dest.j:.1f}]' - return r.format(src=self.source_subshell, - dest=self.destination_subshell) - -class XrayTransitionSet(metaclass=_Descriptor, - attrs=('possible_transitions',)): - - @classmethod - def validate(cls, possible_transitions): - transitions2 = set() - for transition in possible_transitions: + def __repr__(self): + return '{}([n={src.n}, l={src.l}, j={src.j:.1f}] -> [n={dest.n}, l={dest.l}, j={dest.j:.1f}])'\ + .format(self.__class__.__name__, + src=self.source_subshell, + dest=self.destination_subshell) + +@dataclasses.dataclass(frozen=True) +class XrayTransitionSet: + possible_transitions: typing.Tuple[XrayTransition] + + def __post_init__(self): + possible_transitions = set() + for transition in self.possible_transitions: if not isinstance(transition, XrayTransition): transition = XrayTransition(*transition) - transitions2.add(transition) - return (frozenset(transitions2),) - - def _repr_inner(self): - return '{0:d} possible transitions'.format(len(self.possible_transitions)) - -class XrayLine(metaclass=_Descriptor, - attrs=('element', 'transitions', 'iupac', 'siegbahn', 'energy_eV')): - - @classmethod - def validate(cls, element, transitions, iupac, siegbahn, energy_eV): - if not isinstance(element, Element): - element = Element(element) - transitions = tuple(transitions) - return (element, transitions, iupac, siegbahn, energy_eV) - - def _repr_inner(self): - return self.iupac + possible_transitions.add(transition) + + if not possible_transitions: + raise ValueError('At least one transition must be defined') + + object.__setattr__(self, 'possible_transitions', tuple(possible_transitions)) + + def __repr__(self): + return '{}({:d} possible transitions)'.format(self.__class__.__name__, len(self.possible_transitions)) + +@dataclasses.dataclass(frozen=True) +class XrayLine: + element: Element + transitions: typing.Tuple[XrayTransition] + iupac: str + siegbahn: str + energy_eV: float + + def __post_init__(self): + if not isinstance(self.element, Element): + object.__setattr__(self, 'element', Element(self.element)) + + object.__setattr__(self, 'transitions', tuple(self.transitions)) + + def __repr__(self): + return '{}({})'.format(self.__class__.__name__, self.iupac) @property def atomic_number(self): @@ -201,48 +199,58 @@ def atomic_number(self): def z(self): return self.element.atomic_number -class Language(metaclass=_Descriptor, - attrs=('code',)): +@dataclasses.dataclass(frozen=True) +class Language: + code: str - @classmethod - def validate(cls, code): - lencode = len(code) + def __post_init__(self): + lencode = len(self.code) if lencode < 2 or lencode > 3: raise ValueError('Code must be between 2 and 3 characters') - code = code.lower() - return (code,) - -class Notation(metaclass=_Descriptor, - attrs=('name',)): - - @classmethod - def validate(cls, name): - if not name: + + object.__setattr__(self, 'code', self.code.lower()) + + def __repr__(self): + return '{}({})'.format(self.__class__.__name__, self.code) + +@dataclasses.dataclass(frozen=True) +class Notation: + name: str + + def __post_init__(self): + if not self.name: raise ValueError('Name cannot be empty') - name = name.lower() - return (name,) - -class Reference(metaclass=_Descriptor, - attrs=('bibtexkey', 'author', 'year', 'title', 'type', - 'booktitle', 'editor', 'pages', 'edition', - 'journal', 'school', 'address', 'url', 'note', - 'number', 'series', 'volume', 'publisher', - 'organization', 'chapter', 'howpublished', 'doi')): - - @classmethod - def validate(cls, bibtexkey, author=None, year=None, title=None, - type=None, booktitle=None, editor=None, pages=None, #@ReservedAssignment - edition=None, journal=None, school=None, address=None, - url=None, note=None, number=None, series=None, volume=None, - publisher=None, organization=None, chapter=None, - howpublished=None, doi=None): - if not bibtexkey: - raise ValueError('A BibTeX key must be defined') - - return (bibtexkey, author, year, title, type, booktitle, editor, - pages, edition, journal, school, address, url, note, - number, series, volume, publisher, organization, - chapter, howpublished, doi) - - def _repr_inner(self): - return '{0}'.format(self.bibtexkey) + + object.__setattr__(self, 'name', self.name.lower()) + + def __repr__(self): + return '{}({})'.format(self.__class__.__name__, self.name) + +@dataclasses.dataclass(frozen=True) +class Reference: + bibtexkey: str + author: str = None + year: str = None + title: str = None + type: str = None + booktitle: str = None + editor: str = None + pages: str = None + edition: str = None + journal: str = None + school: str = None + address: str = None + url: str = None + note: str = None + number: str = None + series: str = None + volume: str = None + publisher: str = None + organization: str = None + chapter: str = None + howpublished: str = None + doi: str = None + + def __repr__(self): + return '{}({})'.format(self.__class__.__name__, self.bibtexkey) + \ No newline at end of file diff --git a/pyxray/property.py b/pyxray/property.py index 14fd8b2..2a22837 100644 --- a/pyxray/property.py +++ b/pyxray/property.py @@ -3,153 +3,160 @@ """ # Standard library modules. +import dataclasses # Third party modules. # Local modules. -from pyxray.cbook import Immutable, Validable, Reprable +from pyxray.descriptor import \ + (Element, Reference, AtomicShell, AtomicSubshell, XrayTransition, + XrayTransitionSet, Notation, Language) # Globals and constants variables. -class _Property(Immutable, Validable, Reprable): - pass +@dataclasses.dataclass(frozen=True) +class ElementSymbol: + reference: Reference + element: Element + symbol: str -class ElementSymbol(metaclass=_Property, - attrs=('reference', 'element', 'symbol')): - - @classmethod - def validate(cls, reference, element, symbol): - if len(symbol) == 0 or len(symbol) > 3: + def __post_init__(self): + if len(self.symbol) == 0 or len(self.symbol) > 3: raise ValueError('Symbol should be between 1 and 3 characters') - if not symbol[0].isupper(): + + if not self.symbol[0].isupper(): raise ValueError("Symbol should start with a capital letter") -class ElementName(metaclass=_Property, - attrs=('reference', 'element', 'language', 'name')): - - @classmethod - def validate(cls, reference, element, language, name): - if not name: +@dataclasses.dataclass(frozen=True) +class ElementName: + reference: Reference + element: Element + language: Language + name: str + + def __post_init__(self): + if not self.name: raise ValueError('A name must be specified') -class ElementAtomicWeight(metaclass=_Property, - attrs=('reference', 'element', 'value')): +@dataclasses.dataclass(frozen=True) +class ElementAtomicWeight: + reference: Reference + element: Element + value: float - @classmethod - def validate(cls, reference, element, value): - if value <= 0.0: + def __post_init__(self): + if self.value <= 0.0: raise ValueError('Value must be greater than 0.0') -class ElementMassDensity(metaclass=_Property, - attrs=('reference', 'element', 'value_kg_per_m3')): +@dataclasses.dataclass(frozen=True) +class ElementMassDensity: + reference: Reference + element: Element + value_kg_per_m3: float - @classmethod - def validate(cls, reference, element, value_kg_per_m3): - if value_kg_per_m3 <= 0.0: + def __post_init__(self): + if self.value_kg_per_m3 <= 0.0: raise ValueError('Value must be greater than 0.0') - return reference, element, value_kg_per_m3 - -class AtomicShellNotation(metaclass=_Property, - attrs=('reference', 'atomic_shell', 'notation', - 'ascii', 'utf16', 'html', 'latex')): - - @classmethod - def validate(cls, reference, atomic_shell, notation, - ascii, utf16=None, html=None, latex=None): - pass - -class AtomicSubshellNotation(metaclass=_Property, - attrs=('reference', 'atomic_subshell', 'notation', - 'ascii', 'utf16', 'html', 'latex')): - - @classmethod - def validate(cls, reference, atomic_subshell, notation, - ascii, utf16=None, html=None, latex=None): - pass - -class AtomicSubshellBindingEnergy(metaclass=_Property, - attrs=('reference', 'element', 'atomic_subshell', - 'value_eV')): - - @classmethod - def validate(cls, reference, element, atomic_subshell, value_eV): - pass - -class AtomicSubshellRadiativeWidth(metaclass=_Property, - attrs=('reference', 'element', 'atomic_subshell', - 'value_eV')): - - @classmethod - def validate(cls, reference, element, atomic_subshell, value_eV): - pass - -class AtomicSubshellNonRadiativeWidth(metaclass=_Property, - attrs=('reference', 'element', 'atomic_subshell', - 'value_eV')): - - @classmethod - def validate(cls, reference, element, atomic_subshell, value_eV): - pass - -class AtomicSubshellOccupancy(metaclass=_Property, - attrs=('reference', 'element', 'atomic_subshell', - 'value')): - - @classmethod - def validate(cls, reference, element, atomic_subshell, value_eV): - pass - -class XrayTransitionNotation(metaclass=_Property, - attrs=('reference', 'xraytransition', 'notation', - 'ascii', 'utf16', 'html', 'latex')): - - @classmethod - def validate(cls, reference, xraytransition, notation, - ascii, utf16=None, html=None, latex=None): - pass - -class XrayTransitionEnergy(metaclass=_Property, - attrs=('reference', 'element', 'xraytransition', 'value_eV')): - - @classmethod - def validate(cls, reference, element, xraytransition, value_eV): - pass - -class XrayTransitionProbability(metaclass=_Property, - attrs=('reference', 'element', 'xraytransition', 'value')): - - @classmethod - def validate(cls, reference, element, xraytransition, value): - pass - -class XrayTransitionRelativeWeight(metaclass=_Property, - attrs=('reference', 'element', 'xraytransition', 'value')): - - @classmethod - def validate(cls, reference, element, xraytransition, value): - pass - -class XrayTransitionSetNotation(metaclass=_Property, - attrs=('reference', 'xraytransitionset', 'notation', - 'ascii', 'utf16', 'html', 'latex')): - - @classmethod - def validate(cls, reference, xraytransitionset, notation, - ascii, utf16=None, html=None, latex=None): - pass - -class XrayTransitionSetEnergy(metaclass=_Property, - attrs=('reference', 'element', 'xraytransitionset', - 'value_eV')): - - @classmethod - def validate(cls, reference, element, xraytransitionset, value_eV): - pass -class XrayTransitionSetRelativeWeight(metaclass=_Property, - attrs=('reference', 'element', 'xraytransitionset', - 'value')): - - @classmethod - def validate(cls, reference, element, xraytransitionset, value): - pass +@dataclasses.dataclass(frozen=True) +class AtomicShellNotation: + reference: Reference + atomic_shell: AtomicShell + notation: Notation + ascii: str + utf16: str + html: str + latex: str + +@dataclasses.dataclass(frozen=True) +class AtomicSubshellNotation: + reference: Reference + atomic_subshell: AtomicSubshell + notation: Notation + ascii: str + utf16: str + html: str + latex: str + +@dataclasses.dataclass(frozen=True) +class AtomicSubshellBindingEnergy: + reference: Reference + element: Element + atomic_subshell: AtomicSubshell + value_eV: float + +@dataclasses.dataclass(frozen=True) +class AtomicSubshellRadiativeWidth: + reference: Reference + element: Element + atomic_subshell: AtomicSubshell + value_eV: float + +@dataclasses.dataclass(frozen=True) +class AtomicSubshellNonRadiativeWidth: + reference: Reference + element: Element + atomic_subshell: AtomicSubshell + value_eV: float + +@dataclasses.dataclass(frozen=True) +class AtomicSubshellOccupancy: + reference: Reference + element: Element + atomic_subshell: AtomicSubshell + value: float + +@dataclasses.dataclass(frozen=True) +class XrayTransitionNotation: + reference: Reference + xraytransition: XrayTransition + notation: Notation + ascii: str + utf16: str + html: str + latex: str + +@dataclasses.dataclass(frozen=True) +class XrayTransitionEnergy: + reference: Reference + element: Element + xraytransition: XrayTransition + value_eV: float + +@dataclasses.dataclass(frozen=True) +class XrayTransitionProbability: + reference: Reference + element: Element + xraytransition: XrayTransition + value: float + +@dataclasses.dataclass(frozen=True) +class XrayTransitionRelativeWeight: + reference: Reference + element: Element + xraytransition: XrayTransition + value: float + +@dataclasses.dataclass(frozen=True) +class XrayTransitionSetNotation: + reference: Reference + xraytransitionset: XrayTransitionSet + notation: Notation + ascii: str + utf16: str + html: str + latex: str + +@dataclasses.dataclass(frozen=True) +class XrayTransitionSetEnergy: + reference: Reference + element: Element + xraytransitionset: XrayTransitionSet + value_eV: float + +@dataclasses.dataclass(frozen=True) +class XrayTransitionSetRelativeWeight: + reference: Reference + element: Element + xraytransitionset: XrayTransitionSet + value: float diff --git a/pyxray/test_cbook.py b/pyxray/test_cbook.py index 7e2eb67..0db9863 100644 --- a/pyxray/test_cbook.py +++ b/pyxray/test_cbook.py @@ -2,212 +2,40 @@ """ """ # Standard library modules. -import unittest -import logging - # Third party modules. +import pytest # Local modules. -from pyxray.cbook import \ - Immutable, Cachable, Validable, ProgressMixin, ProgressReportMixin +from pyxray.cbook import ProgressMixin, ProgressReportMixin # Globals and constants variables. -class MockImmutable(metaclass=Immutable, - attrs=('foo', 'bar')): - pass - -class MockCachable(metaclass=Cachable): - - def __init__(self, foo): - self.foo = foo - -class MockValidable(metaclass=Validable): - - @classmethod - def validate(cls, foo): - if foo != 'abc': - raise ValueError - return (foo + 'd',) - - def __init__(self, foo): - self.foo = foo - -class MockValidable2(metaclass=Validable): - - @classmethod - def validate(cls, foo): - pass - - def __init__(self, foo): - self.foo = foo - -class MockValidable3(metaclass=Validable): - - @classmethod - def validate(cls, foo): - return (), {'foo': foo + 'd'} - - def __init__(self, foo): - self.foo = foo - -class CombinedType(Immutable, Cachable, Validable): - pass - -class MockCombined(metaclass=CombinedType, attrs=('foo', 'bar')): - - @classmethod - def validate(cls, foo, bar): - if len(foo) != 3: - raise ValueError - return (foo + 'd', bar) - class MockProgress(ProgressMixin): pass class MockProgressReport(ProgressReportMixin): pass -class TestImmutable(unittest.TestCase): - - def setUp(self): - super().setUp() - - self.obj = MockImmutable('abc', 123) - - def test__init__(self): - self.assertEqual('abc', self.obj.foo) - self.assertEqual(123, self.obj.bar) - - self.assertRaises(TypeError, MockImmutable, 'abc') - self.assertRaises(TypeError, MockImmutable, 'abc', 'abc', 'abc') - - obj = MockImmutable('abc', bar=123) - self.assertEqual('abc', obj.foo) - self.assertEqual(123, obj.bar) - - obj = MockImmutable(foo='abc', bar=123) - self.assertEqual('abc', obj.foo) - self.assertEqual(123, obj.bar) - - self.assertRaises(TypeError, MockImmutable, 'abc', abc='def') - - def test__slots__(self): - self.assertRaises(AttributeError, setattr, self.obj, 'foo2', 'abc') - - def testfoo(self): - self.assertEqual('abc', self.obj.foo) - self.assertRaises(AttributeError, setattr, self.obj, 'foo', 'def') - self.assertRaises(AttributeError, delattr, self.obj, 'foo') - - def testbar(self): - self.assertEqual(123, self.obj.bar) - self.assertRaises(AttributeError, setattr, self.obj, 'bar', 456) - self.assertRaises(AttributeError, delattr, self.obj, 'bar') - -class TestCachable(unittest.TestCase): - - def setUp(self): - super().setUp() - - self.obj = MockCachable('abc') - - def test__init__(self): - self.assertEqual('abc', self.obj.foo) - - def testcache(self): - obj2 = MockCachable('abc') - self.assertEqual('abc', obj2.foo) - self.assertEqual(obj2, self.obj) - self.assertIs(obj2, self.obj) - - obj3 = MockCachable('def') - self.assertEqual('def', obj3.foo) - self.assertNotEqual(obj3, self.obj) - self.assertIsNot(obj3, self.obj) - -class TestValidable(unittest.TestCase): - - def setUp(self): - super().setUp() - - self.obj = MockValidable('abc') - self.obj2 = MockValidable2('abc') - self.obj3 = MockValidable3('abc') - - def test__init__(self): - self.assertEqual('abcd', self.obj.foo) - self.assertEqual('abc', self.obj2.foo) - self.assertEqual('abcd', self.obj3.foo) - - def testvalidate(self): - self.assertRaises(ValueError, MockValidable, 'def') - -class TestCombined(unittest.TestCase): - - def setUp(self): - super().setUp() - - self.obj = MockCombined('abc', 123) - - def test__init__(self): - self.assertRaises(TypeError, MockCombined, 'abc') - self.assertRaises(TypeError, MockCombined, 'abc', 'abc', 'abc') - - def test__slots__(self): - self.assertRaises(AttributeError, setattr, self.obj, 'foo2', 'abc') - - def testfoo(self): - self.assertEqual('abcd', self.obj.foo) - self.assertRaises(AttributeError, setattr, self.obj, 'foo', 'def') - self.assertRaises(AttributeError, delattr, self.obj, 'foo') - - def testbar(self): - self.assertEqual(123, self.obj.bar) - self.assertRaises(AttributeError, setattr, self.obj, 'bar', 456) - self.assertRaises(AttributeError, delattr, self.obj, 'bar') - - def testcache(self): - obj2 = MockCombined('abc', 123) - self.assertEqual('abcd', obj2.foo) - self.assertEqual(obj2, self.obj) - self.assertIs(obj2, self.obj) - - obj3 = MockCombined('def', 123) - self.assertEqual('defd', obj3.foo) - self.assertNotEqual(obj3, self.obj) - self.assertIsNot(obj3, self.obj) - -class TestProgressMixin(unittest.TestCase): - - def setUp(self): - super().setUp() - - self.obj = MockProgress() - - def testupdate(self): - self.obj.update(50) - self.assertEqual(50, self.obj.progress) +@pytest.fixture +def progress(): + return MockProgress() -class TestProgressReportMixin(unittest.TestCase): +def test_progress_update(progress): + progress.update(50) + assert progress.progress == 50 - def setUp(self): - super().setUp() +@pytest.fixture +def progress_report(progress): + report = MockProgressReport() + report.add_reporthook(lambda p: progress.update(p)) + return report - self.obj = MockProgressReport() - self.obj.add_reporthook(self._hook) - self.progress = 0 +def test_progress_report_update(progress_report, progress): + assert progress.progress == 0 - def _hook(self, progress): - self.progress = progress + progress_report.update(50) - def testupdate(self): - self.assertEqual(0, self.progress) - self.obj.update(50) - self.assertEqual(50, self.obj.progress) - self.assertEqual(50, self.progress) + assert progress_report.progress == 50 + assert progress.progress == 50 -if __name__ == '__main__': #pragma: no cover - logging.getLogger().setLevel(logging.DEBUG) - unittest.main() diff --git a/pyxray/test_descriptor.py b/pyxray/test_descriptor.py index f61286b..70c55e1 100644 --- a/pyxray/test_descriptor.py +++ b/pyxray/test_descriptor.py @@ -2,199 +2,337 @@ """ """ # Standard library modules. -import unittest -import logging +import dataclasses # Third party modules. +import pytest # Local modules. from pyxray.descriptor import \ - (Element, AtomicShell, AtomicSubshell, Reference, XrayLine, XrayTransition) + (Element, AtomicShell, AtomicSubshell, Reference, XrayLine, XrayTransition, + XrayTransitionSet, Language, Notation) # Globals and constants variables. -class TestElement(unittest.TestCase): - - def setUp(self): - super().setUp() - - self.element = Element(6) - - def test__init__(self): - self.assertEqual(6, self.element.z) - self.assertEqual(6, self.element.atomic_number) +@pytest.fixture +def element(): + return Element(6) - def test__hash__(self): - self.assertEqual(hash(Element(6)), hash(self.element)) +def test_element(element): + assert element.z == 6 + assert element.atomic_number == 6 - def testvalidable(self): - self.assertRaises(ValueError, Element, 0) - self.assertRaises(ValueError, Element, 119) - self.assertRaises(ValueError, Element.validate, 0) - self.assertRaises(ValueError, Element.validate, 119) +def test_element_eq(element): + assert element == Element(6) - def testimmutable(self): - self.assertRaises(AttributeError, setattr, - self.element, 'atomic_number', 7) - self.assertRaises(AttributeError, delattr, - self.element, 'atomic_number') - self.assertRaises(AttributeError, setattr, - self.element, 'abc', 7) - - def testcachable(self): - self.assertEqual(Element(6), self.element) - self.assertIs(Element(6), self.element) - - def testreprable(self): - self.assertEqual('Element(z=6)', repr(self.element)) - -class TestAtomicShell(unittest.TestCase): - - def setUp(self): - super().setUp() +def test_element_hash(element): + assert hash(element) == hash(Element(6)) - self.atomicshell = AtomicShell(3) +def test_element_repr(element): + assert repr(element) == 'Element(z=6)' - def test__init__(self): - self.assertEqual(3, self.atomicshell.n) - self.assertEqual(3, self.atomicshell.principal_quantum_number) +def test_element_validate(): + with pytest.raises(ValueError): + Element(0) - def testvalidable(self): - self.assertRaises(ValueError, AtomicShell, 0) - self.assertRaises(ValueError, AtomicShell.validate, 0) + with pytest.raises(ValueError): + Element(119) - def testimmutable(self): - self.assertRaises(AttributeError, setattr, - self.atomicshell, 'principal_quantum_number', 4) - self.assertRaises(AttributeError, delattr, - self.atomicshell, 'principal_quantum_number') - self.assertRaises(AttributeError, setattr, - self.atomicshell, 'abc', 7) - - def testcachable(self): - self.assertEqual(AtomicShell(3), self.atomicshell) - self.assertIs(AtomicShell(3), self.atomicshell) - - def testreprable(self): - self.assertEqual('AtomicShell(n=3)', repr(self.atomicshell)) - -class TestAtomicSubshell(unittest.TestCase): - - def setUp(self): - super().setUp() - - self.atomicsubshell = AtomicSubshell(3, 0, 1) - - def test__init__(self): - self.assertEqual(AtomicShell(3), self.atomicsubshell.atomic_shell) - self.assertEqual(3, self.atomicsubshell.n) - self.assertEqual(3, self.atomicsubshell.principal_quantum_number) - self.assertEqual(0, self.atomicsubshell.l) - self.assertEqual(0, self.atomicsubshell.azimuthal_quantum_number) - self.assertEqual(1, self.atomicsubshell.j_n) - self.assertEqual(1, self.atomicsubshell.total_angular_momentum_nominator) - self.assertAlmostEqual(0.5, self.atomicsubshell.j) - self.assertAlmostEqual(0.5, self.atomicsubshell.total_angular_momentum) - - def testvalidable(self): - self.assertRaises(ValueError, AtomicSubshell, 3, 0, 5) - self.assertRaises(ValueError, AtomicSubshell.validate, 3, 0, 5) - self.assertRaises(ValueError, AtomicSubshell, 3, -1, 1) - self.assertRaises(ValueError, AtomicSubshell.validate, 3, -1, 1) - self.assertRaises(ValueError, AtomicSubshell, 3, 3, 1) - self.assertRaises(ValueError, AtomicSubshell.validate, 3, 3, 1) - - def testimmutable(self): - self.assertRaises(AttributeError, setattr, - self.atomicsubshell, 'principal_quantum_number', 4) - self.assertRaises(AttributeError, delattr, - self.atomicsubshell, 'principal_quantum_number') - self.assertRaises(AttributeError, setattr, - self.atomicsubshell, 'abc', 7) - - def testcachable(self): - self.assertEqual(AtomicSubshell(3, 0, 1), self.atomicsubshell) - self.assertIs(AtomicSubshell(3, 0, 1), self.atomicsubshell) - - def testreprable(self): - self.assertEqual('AtomicSubshell(n=3, l=0, j=0.5)', repr(self.atomicsubshell)) - -class TestReference(unittest.TestCase): - - def setUp(self): - super().setUp() - - self.ref = Reference('doe2016') - - def test__init__(self): - self.assertEqual('doe2016', self.ref.bibtexkey) - self.assertIsNone(self.ref.author) - - def testvalidable(self): - self.assertRaises(ValueError, Reference, '') - self.assertRaises(ValueError, Reference.validate, '') - self.assertRaises(ValueError, Reference, None) - self.assertRaises(ValueError, Reference.validate, None) - - def testimmutable(self): - self.assertRaises(AttributeError, setattr, - self.ref, 'bibtexkey', 'john') - self.assertRaises(AttributeError, delattr, - self.ref, 'bibtexkey') - self.assertRaises(AttributeError, setattr, - self.ref, 'abc', 7) - - def testcachable(self): - self.assertEqual(Reference('doe2016'), self.ref) - self.assertIs(Reference('doe2016'), self.ref) - - def testreprable(self): - self.assertEqual('Reference(doe2016)', repr(self.ref)) - -class TestXrayLine(unittest.TestCase): - - def setUp(self): - super().setUp() - - element = Element(118) - K = AtomicSubshell(1, 0, 1) - L3 = AtomicSubshell(2, 1, 3) - transition = XrayTransition(L3, K) - self.xrayline = XrayLine(element, [transition], 'a', 'b', 0.1) - - def test__init__(self): - self.assertEqual(118, self.xrayline.element.atomic_number) - self.assertEqual(1, len(self.xrayline.transitions)) - self.assertEqual('a', self.xrayline.iupac) - self.assertEqual('b', self.xrayline.siegbahn) - self.assertAlmostEqual(0.1, self.xrayline.energy_eV, 4) - - def test__hash__(self): - K = AtomicSubshell(1, 0, 1) - L3 = AtomicSubshell(2, 1, 3) - transition = XrayTransition(L3, K) - other = XrayLine(118, [transition], 'a', 'b', 0.1) - self.assertEqual(hash(self.xrayline), hash(other)) - self.assertEqual(hash(self.xrayline), hash(self.xrayline)) - - def testimmutable(self): - self.assertRaises(AttributeError, setattr, - self.xrayline, 'element', 'john') - self.assertRaises(AttributeError, delattr, - self.xrayline, 'element') - - def testcachable(self): - K = AtomicSubshell(1, 0, 1) - L3 = AtomicSubshell(2, 1, 3) - transition = XrayTransition(L3, K) - other = XrayLine(118, [transition], 'a', 'b', 0.1) - - self.assertEqual(other, self.xrayline) - self.assertIs(other, self.xrayline) - - def testreprable(self): - self.assertEqual('XrayLine(a)', repr(self.xrayline)) - -if __name__ == '__main__': #pragma: no cover - logging.getLogger().setLevel(logging.DEBUG) - unittest.main() +def test_element_frozen(element): + with pytest.raises(dataclasses.FrozenInstanceError): + element.atomic_number = 7 + + with pytest.raises(dataclasses.FrozenInstanceError): + del element.atomic_number + + with pytest.raises(dataclasses.FrozenInstanceError): + element.abc = 7 + +@pytest.fixture +def atomicshell(): + return AtomicShell(3) + +def test_atomicshell(atomicshell): + assert atomicshell.n == 3 + assert atomicshell.principal_quantum_number == 3 + +def test_atomicshell_eq(atomicshell): + assert atomicshell == AtomicShell(3) + +def test_atomicshell_hash(atomicshell): + assert hash(atomicshell) == hash(AtomicShell(3)) + +def test_atomicshell_repr(atomicshell): + assert repr(atomicshell) == 'AtomicShell(n=3)' + +def test_atomicshell_validable(): + with pytest.raises(ValueError): + AtomicShell(0) + +def test_atomicshell_frozen(atomicshell): + with pytest.raises(dataclasses.FrozenInstanceError): + atomicshell.n = 7 + + with pytest.raises(dataclasses.FrozenInstanceError): + del atomicshell.n + + with pytest.raises(dataclasses.FrozenInstanceError): + atomicshell.abc = 7 + +@pytest.fixture +def atomicsubshell(): + return AtomicSubshell(3, 0, 1) + +def test_atomicsubshell(atomicsubshell): + assert atomicsubshell.n == 3 + assert atomicsubshell.atomic_shell == AtomicShell(3) + assert atomicsubshell.principal_quantum_number == 3 + assert atomicsubshell.l == 0 + assert atomicsubshell.azimuthal_quantum_number == 0 + assert atomicsubshell.j_n == 1 + assert atomicsubshell.total_angular_momentum_nominator == 1 + assert atomicsubshell.j == 0.5 + assert atomicsubshell.total_angular_momentum == 0.5 + +def test_atomicsubshell_eq(atomicsubshell): + assert atomicsubshell == AtomicSubshell(3, 0, 1) + assert atomicsubshell == AtomicSubshell(AtomicShell(3), 0, 1) + +def test_atomicsubshell_hash(atomicsubshell): + assert hash(atomicsubshell) == hash(AtomicSubshell(3, 0, 1)) + assert hash(atomicsubshell) == hash(AtomicSubshell(AtomicShell(3), 0, 1)) + +def test_atomicsubshell_repr(atomicsubshell): + assert repr(atomicsubshell) == 'AtomicSubshell(n=3, l=0, j=0.5)' + +def test_atomicsubshell_validate(): + with pytest.raises(ValueError): + AtomicSubshell(3, 0, 5) + + with pytest.raises(ValueError): + AtomicSubshell(3, -1, 1) + + with pytest.raises(ValueError): + AtomicSubshell(3, 3, 1) + +def test_atomicsubshell_frozen(atomicsubshell): + with pytest.raises(dataclasses.FrozenInstanceError): + atomicsubshell.n = 7 + + with pytest.raises(dataclasses.FrozenInstanceError): + del atomicsubshell.n + + with pytest.raises(dataclasses.FrozenInstanceError): + atomicsubshell.abc = 7 + +@pytest.fixture +def xraytransition(): + source_subshell = AtomicSubshell(2, 0, 1) + destination_subshell = AtomicSubshell(1, 0, 1) + return XrayTransition(source_subshell, destination_subshell) + +def test_xraytransition(xraytransition): + assert xraytransition.source_subshell.n == 2 + assert xraytransition.source_subshell.l == 0 + assert xraytransition.source_subshell.j_n == 1 + assert xraytransition.destination_subshell.n == 1 + assert xraytransition.destination_subshell.l == 0 + assert xraytransition.destination_subshell.j_n == 1 + +def test_xraytransition_eq(xraytransition): + assert xraytransition == XrayTransition((2, 0, 1), (1, 0, 1)) + +def test_xraytransition_hash(xraytransition): + assert hash(xraytransition) == hash(XrayTransition((2, 0, 1), (1, 0, 1))) + assert hash(xraytransition) == hash(XrayTransition(AtomicSubshell(2, 0, 1), AtomicSubshell(1, 0, 1))) + +def test_xraytransition_repr(xraytransition): + assert repr(xraytransition) == 'XrayTransition([n=2, l=0, j=0.5] -> [n=1, l=0, j=0.5])' + +def test_xraytransition_frozen(xraytransition): + with pytest.raises(dataclasses.FrozenInstanceError): + xraytransition.source_subshell = AtomicSubshell(2, 0, 1) + + with pytest.raises(dataclasses.FrozenInstanceError): + del xraytransition.source_subshell + + with pytest.raises(dataclasses.FrozenInstanceError): + xraytransition.abc = 7 + +@pytest.fixture +def xraytransitionset(): + source_subshell = AtomicSubshell(2, 0, 1) + destination_subshell = AtomicSubshell(1, 0, 1) + transition1 = XrayTransition(source_subshell, destination_subshell) + + source_subshell = AtomicSubshell(3, 0, 1) + destination_subshell = AtomicSubshell(1, 0, 1) + transition2 = XrayTransition(source_subshell, destination_subshell) + + return XrayTransitionSet([transition1, transition2]) + +def test_xraytransitionset(xraytransitionset): + assert len(xraytransitionset.possible_transitions) == 2 + +def test_xraytransitionset_eq(xraytransitionset): + transition1 = XrayTransition((2, 0, 1), (1, 0, 1)) + transition2 = XrayTransition((3, 0, 1), (1, 0, 1)) + assert xraytransitionset == XrayTransitionSet((transition1, transition2)) + +def test_xraytransitionset_hash(xraytransitionset): + transition1 = XrayTransition((2, 0, 1), (1, 0, 1)) + transition2 = XrayTransition((3, 0, 1), (1, 0, 1)) + assert hash(xraytransitionset) == hash(XrayTransitionSet((transition1, transition2))) + assert hash(xraytransitionset) == hash(XrayTransitionSet([transition1, transition2])) + +def test_xraytransitionset_repr(xraytransitionset): + assert repr(xraytransitionset) == 'XrayTransitionSet(2 possible transitions)' + +def test_xraytransitionset_validate(xraytransitionset): + with pytest.raises(ValueError): + XrayTransitionSet(()) + +def test_xraytransitionset_frozen(xraytransitionset): + with pytest.raises(dataclasses.FrozenInstanceError): + xraytransitionset.possible_transitions = (XrayTransition((2, 0, 1), (1, 0, 1)),) + + with pytest.raises(dataclasses.FrozenInstanceError): + del xraytransitionset.possible_transitions + + with pytest.raises(dataclasses.FrozenInstanceError): + xraytransitionset.abc = 7 + +@pytest.fixture +def xrayline(): + element = Element(118) + K = AtomicSubshell(1, 0, 1) + L3 = AtomicSubshell(2, 1, 3) + transition = XrayTransition(L3, K) + return XrayLine(element, [transition], 'a', 'b', 0.1) + +def test_xrayline(xrayline): + assert xrayline.element.z == 118 + assert len(xrayline.transitions) == 1 + assert XrayTransition((2, 1, 3), (1, 0, 1)) in xrayline.transitions + assert xrayline.iupac == 'a' + assert xrayline.siegbahn == 'b' + assert xrayline.energy_eV == pytest.approx(0.1, abs=1e-4) + +def test_xrayline_eq(xrayline): + assert xrayline == XrayLine(118, (XrayTransition((2, 1, 3), (1, 0, 1)),), 'a', 'b', 0.1) + + assert xrayline != XrayLine(117, (XrayTransition((2, 1, 3), (1, 0, 1)),), 'a', 'b', 0.1) + assert xrayline != XrayLine(118, (XrayTransition((3, 1, 3), (1, 0, 1)),), 'a', 'b', 0.1) + assert xrayline != XrayLine(118, (XrayTransition((2, 1, 3), (1, 0, 1)),), 'z', 'b', 0.1) + assert xrayline != XrayLine(118, (XrayTransition((2, 1, 3), (1, 0, 1)),), 'a', 'z', 0.1) + assert xrayline != XrayLine(118, (XrayTransition((2, 1, 3), (1, 0, 1)),), 'a', 'b', 999) + +def test_xrayline_hash(xrayline): + assert hash(xrayline) == hash(XrayLine(118, (XrayTransition((2, 1, 3), (1, 0, 1)),), 'a', 'b', 0.1)) + +def test_xrayline_repr(xrayline): + assert repr(xrayline) == 'XrayLine(a)' + +def test_xrayline_frozen(xrayline): + with pytest.raises(dataclasses.FrozenInstanceError): + xrayline.element = 117 + + with pytest.raises(dataclasses.FrozenInstanceError): + del xrayline.element + + with pytest.raises(dataclasses.FrozenInstanceError): + xrayline.abc = 7 + +@pytest.fixture +def language(): + return Language('en') + +def test_language(language): + assert language.code == 'en' + +def test_language_eq(language): + assert language == Language('en') + assert language == Language('EN') + assert language != Language('fr') + +def test_language_hash(language): + assert hash(language) == hash(Language('EN')) + +def test_language_repr(language): + assert repr(language) == 'Language(en)' + +def test_language_validate(): + with pytest.raises(ValueError): + Language('english') + + with pytest.raises(ValueError): + Language('e') + +def test_language_frozen(language): + with pytest.raises(dataclasses.FrozenInstanceError): + language.code = 'fr' + + with pytest.raises(dataclasses.FrozenInstanceError): + del language.code + + with pytest.raises(dataclasses.FrozenInstanceError): + language.abc = 7 + +@pytest.fixture +def notation(): + return Notation('foo') + +def test_notation(notation): + assert notation.name == 'foo' + +def test_notation_eq(notation): + assert notation == Notation('foo') + assert notation == Notation('FOO') + assert notation != Notation('bar') + +def test_notation_hash(notation): + assert hash(notation) == hash(Notation('FOO')) + +def test_notation_repr(notation): + assert repr(notation) == 'Notation(foo)' + +def test_notation_validate(): + with pytest.raises(ValueError): + Notation('') + +def test_notation_frozen(notation): + with pytest.raises(dataclasses.FrozenInstanceError): + notation.name = 'bar' + + with pytest.raises(dataclasses.FrozenInstanceError): + del notation.name + + with pytest.raises(dataclasses.FrozenInstanceError): + notation.abc = 7 + +@pytest.fixture +def reference(): + return Reference('doe2016') + +def test_reference(reference): + assert reference.bibtexkey == 'doe2016' + +def test_reference_eq(reference): + assert reference == Reference('doe2016') + assert reference != Reference('doe2016', year=2016) + +def test_reference_hash(reference): + assert hash(reference) == hash(Reference('doe2016')) + +def test_reference_repr(reference): + assert repr(reference) == 'Reference(doe2016)' + +def test_reference_frozen(reference): + with pytest.raises(dataclasses.FrozenInstanceError): + reference.author = 'bar' + + with pytest.raises(dataclasses.FrozenInstanceError): + del reference.author + + with pytest.raises(dataclasses.FrozenInstanceError): + reference.abc = 7 diff --git a/pyxray/test_property.py b/pyxray/test_property.py index e5e20fd..0d184dd 100644 --- a/pyxray/test_property.py +++ b/pyxray/test_property.py @@ -2,10 +2,9 @@ """ """ # Standard library modules. -import unittest -import logging # Third party modules. +import pytest # Local modules. from pyxray.descriptor import Reference, Element @@ -13,49 +12,22 @@ # Globals and constants variables. -REFERENCE_TEST = Reference('test2016') - -class TestElementSymbol(unittest.TestCase): - - def setUp(self): - super().setUp() - - self.prop = ElementSymbol(REFERENCE_TEST, Element(6), 'C') - - def test__init__(self): - self.assertEqual(REFERENCE_TEST, self.prop.reference) - self.assertIs(REFERENCE_TEST, self.prop.reference) - self.assertEqual(Element(6), self.prop.element) - self.assertIs(Element(6), self.prop.element) - self.assertEqual('C', self.prop.symbol) - - def testvalidable(self): - self.assertRaises(ValueError, ElementSymbol, - REFERENCE_TEST, Element(6), '') - self.assertRaises(ValueError, ElementSymbol, - REFERENCE_TEST, Element(6), 'CCCC') - self.assertRaises(ValueError, ElementSymbol, - REFERENCE_TEST, Element(6), 'c') - - def testimmutable(self): - self.assertRaises(AttributeError, setattr, - self.prop, 'reference', None) - self.assertRaises(AttributeError, delattr, - self.prop, 'reference') - self.assertRaises(AttributeError, setattr, - self.prop, 'element', None) - self.assertRaises(AttributeError, delattr, - self.prop, 'element') - self.assertRaises(AttributeError, setattr, - self.prop, 'symbol', None) - self.assertRaises(AttributeError, delattr, - self.prop, 'symbol') - self.assertRaises(AttributeError, setattr, - self.prop, 'abc', 7) - - def testreprable(self): - self.assertEqual('ElementSymbol(test2016, z=6, symbol=C)', repr(self.prop)) - -if __name__ == '__main__': #pragma: no cover - logging.getLogger().setLevel(logging.DEBUG) - unittest.main() +@pytest.fixture +def reference(): + return Reference('test2016') + +@pytest.fixture +def element_symbol(reference): + return ElementSymbol(reference, Element(6), 'C') + +def test_element_symbol(element_symbol, reference): + assert element_symbol.reference == reference + assert element_symbol.element == Element(6) + assert element_symbol.symbol == 'C' + +def test_element_symbol_validate(reference): + with pytest.raises(ValueError): + ElementSymbol(reference, Element(6), '') + ElementSymbol(reference, Element(6), 'CCC') + ElementSymbol(reference, Element(6), 'c') + From 98f2c33a3a76e1867041b32112bb57fbc687844b Mon Sep 17 00:00:00 2001 From: Philippe Pinard Date: Sat, 9 Mar 2019 19:33:29 +0000 Subject: [PATCH 2/5] Update travis script --- .travis.yml | 4 ++-- setup.py | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index af568f6..8e841b1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,13 +5,13 @@ cache: - $HOME/.pip-cache/ matrix: include: - - python: '3.6' + - python: '3.7' os: linux install: - pip install --cache-dir $HOME/.pip-cache --upgrade pip codecov - pip install --cache-dir $HOME/.pip-cache --upgrade -e .[build,develop] script: -- nosetests +- pytest after_success: codecov notifications: email: false diff --git a/setup.py b/setup.py index 601ffd5..52dc83f 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ def run(self): INSTALL_REQUIRES = ['tabulate'] EXTRAS_REQUIRE = {'develop': ['requests', 'requests-cache', 'progressbar2', - 'nose', 'coverage'] + 'pytest', 'coverage'] } CMDCLASS = versioneer.get_cmdclass() @@ -82,8 +82,6 @@ def run(self): install_requires=INSTALL_REQUIRES, extras_require=EXTRAS_REQUIRE, - test_suite='nose.collector', - cmdclass=CMDCLASS, entry_points=ENTRY_POINTS, From 3e170081415b2e271279c89807007bb9dc19a174 Mon Sep 17 00:00:00 2001 From: Philippe Pinard Date: Sat, 9 Mar 2019 19:36:57 +0000 Subject: [PATCH 3/5] Fix travis script again --- .travis.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8e841b1..b3c86cb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,17 +1,17 @@ sudo: false +dist: xenial language: python -cache: - directories: - - $HOME/.pip-cache/ +cache: pip matrix: include: - python: '3.7' os: linux install: -- pip install --cache-dir $HOME/.pip-cache --upgrade pip codecov -- pip install --cache-dir $HOME/.pip-cache --upgrade -e .[build,develop] +- pip install --upgrade pip codecov +- pip install --upgrade -e .[build,develop] script: - pytest -after_success: codecov +after_success: +- codecov notifications: email: false From 4223827110705569759f507f5bdbad45adac53fa Mon Sep 17 00:00:00 2001 From: Philippe Pinard Date: Sun, 17 Mar 2019 14:03:46 +0000 Subject: [PATCH 4/5] Make it work on Python 3.6 --- .travis.yml | 7 +++---- setup.cfg | 10 +++------- setup.py | 4 ++-- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/.travis.yml b/.travis.yml index b3c86cb..2cf0bfc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,10 +2,9 @@ sudo: false dist: xenial language: python cache: pip -matrix: - include: - - python: '3.7' - os: linux +python: + - '3.6' + - '3.7' install: - pip install --upgrade pip codecov - pip install --upgrade -e .[build,develop] diff --git a/setup.cfg b/setup.cfg index fd06ffb..85ccbea 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,13 +1,9 @@ [bdist_wheel] python-tag=py3 -[nosetests] -with-xunit = 1 -with-coverage = 1 -cover-xml = 1 -cover-erase = 1 -cover-package = pyxray -logging-level = INFO +[tool:pytest] +norecursedirs = .* build dist CVS _darcs *.egg venv old +addopts = --cov --cov-report xml [versioneer] VCS = git diff --git a/setup.py b/setup.py index 52dc83f..9355595 100644 --- a/setup.py +++ b/setup.py @@ -31,9 +31,9 @@ def run(self): with open(os.path.join(BASEDIR, 'README.rst'), 'r') as fp: LONG_DESCRIPTION = fp.read() -INSTALL_REQUIRES = ['tabulate'] +INSTALL_REQUIRES = ['tabulate', 'dataclasses;python_version~="3.6"'] EXTRAS_REQUIRE = {'develop': ['requests', 'requests-cache', 'progressbar2', - 'pytest', 'coverage'] + 'pytest', 'pytest-cov'] } CMDCLASS = versioneer.get_cmdclass() From e639ea39240bc812cb1c5ae572eb37ef19450f8b Mon Sep 17 00:00:00 2001 From: Philippe Pinard Date: Sun, 17 Mar 2019 14:09:36 +0000 Subject: [PATCH 5/5] Add appveyor support --- appveyor.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 appveyor.yml diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..2d19434 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,14 @@ +environment: + + matrix: + - PYTHON: "C:\\Python36-x64" + - PYTHON: "C:\\Python37-x64" + +install: + - "%PYTHON%\\python.exe -m pip install --upgrade pip wheel codecov" + - "%PYTHON%\\python.exe -m pip install --upgrade -e .[build,develop]" + +build: off + +test_script: + - "%PYTHON%\\Scripts\\pytest.exe" \ No newline at end of file