gemd 2.0.0__tar.gz → 2.1.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. {gemd-2.0.0 → gemd-2.1.3}/PKG-INFO +10 -10
  2. {gemd-2.0.0 → gemd-2.1.3}/README.md +1 -1
  3. gemd-2.1.3/gemd/__version__.py +1 -0
  4. {gemd-2.0.0 → gemd-2.1.3}/gemd/demo/cake.py +2 -4
  5. {gemd-2.0.0 → gemd-2.1.3}/gemd/demo/strehlow_and_cook.py +2 -3
  6. {gemd-2.0.0 → gemd-2.1.3}/gemd/units/impl.py +97 -54
  7. {gemd-2.0.0 → gemd-2.1.3}/gemd.egg-info/PKG-INFO +10 -10
  8. {gemd-2.0.0 → gemd-2.1.3}/gemd.egg-info/SOURCES.txt +5 -1
  9. {gemd-2.0.0 → gemd-2.1.3}/gemd.egg-info/requires.txt +8 -1
  10. gemd-2.1.3/gemd.egg-info/top_level.txt +2 -0
  11. {gemd-2.0.0 → gemd-2.1.3}/setup.py +18 -13
  12. gemd-2.1.3/tests/__init__.py +0 -0
  13. gemd-2.1.3/tests/units/__init__.py +0 -0
  14. gemd-2.1.3/tests/units/test_parser.py +225 -0
  15. gemd-2.1.3/tests/units/test_units.txt +4 -0
  16. gemd-2.0.0/gemd/__version__.py +0 -1
  17. gemd-2.0.0/gemd.egg-info/top_level.txt +0 -1
  18. {gemd-2.0.0 → gemd-2.1.3}/LICENSE +0 -0
  19. {gemd-2.0.0 → gemd-2.1.3}/gemd/__init__.py +0 -0
  20. {gemd-2.0.0 → gemd-2.1.3}/gemd/builders/__init__.py +0 -0
  21. {gemd-2.0.0 → gemd-2.1.3}/gemd/builders/impl.py +0 -0
  22. {gemd-2.0.0 → gemd-2.1.3}/gemd/demo/__init__.py +0 -0
  23. {gemd-2.0.0 → gemd-2.1.3}/gemd/demo/material_run_example.py +0 -0
  24. {gemd-2.0.0 → gemd-2.1.3}/gemd/demo/measurement_example.py +0 -0
  25. {gemd-2.0.0 → gemd-2.1.3}/gemd/demo/strehlow_and_cook.pif +0 -0
  26. {gemd-2.0.0 → gemd-2.1.3}/gemd/demo/strehlow_and_cook_small.pif +0 -0
  27. {gemd-2.0.0 → gemd-2.1.3}/gemd/demo/table_example.py +0 -0
  28. {gemd-2.0.0 → gemd-2.1.3}/gemd/demo/toothpick.jpg +0 -0
  29. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/__init__.py +0 -0
  30. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/attribute/__init__.py +0 -0
  31. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/attribute/base_attribute.py +0 -0
  32. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/attribute/condition.py +0 -0
  33. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/attribute/parameter.py +0 -0
  34. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/attribute/property.py +0 -0
  35. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/attribute/property_and_conditions.py +0 -0
  36. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/base_entity.py +0 -0
  37. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/bounds/__init__.py +0 -0
  38. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/bounds/base_bounds.py +0 -0
  39. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/bounds/categorical_bounds.py +0 -0
  40. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/bounds/composition_bounds.py +0 -0
  41. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/bounds/integer_bounds.py +0 -0
  42. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/bounds/molecular_structure_bounds.py +0 -0
  43. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/bounds/real_bounds.py +0 -0
  44. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/bounds_validation.py +0 -0
  45. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/case_insensitive_dict.py +0 -0
  46. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/dict_serializable.py +0 -0
  47. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/file_link.py +0 -0
  48. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/has_dependencies.py +0 -0
  49. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/link_by_uid.py +0 -0
  50. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/object/__init__.py +0 -0
  51. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/object/base_object.py +0 -0
  52. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/object/has_conditions.py +0 -0
  53. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/object/has_material.py +0 -0
  54. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/object/has_parameters.py +0 -0
  55. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/object/has_process.py +0 -0
  56. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/object/has_properties.py +0 -0
  57. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/object/has_quantities.py +0 -0
  58. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/object/has_source.py +0 -0
  59. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/object/has_spec.py +0 -0
  60. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/object/has_template.py +0 -0
  61. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/object/has_template_check_generator.py +0 -0
  62. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/object/ingredient_run.py +0 -0
  63. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/object/ingredient_spec.py +0 -0
  64. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/object/material_run.py +0 -0
  65. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/object/material_spec.py +0 -0
  66. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/object/measurement_run.py +0 -0
  67. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/object/measurement_spec.py +0 -0
  68. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/object/process_run.py +0 -0
  69. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/object/process_spec.py +0 -0
  70. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/setters.py +0 -0
  71. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/source/__init__.py +0 -0
  72. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/source/performed_source.py +0 -0
  73. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/template/__init__.py +0 -0
  74. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/template/attribute_template.py +0 -0
  75. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/template/base_template.py +0 -0
  76. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/template/condition_template.py +0 -0
  77. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/template/has_condition_templates.py +0 -0
  78. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/template/has_parameter_templates.py +0 -0
  79. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/template/has_property_templates.py +0 -0
  80. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/template/material_template.py +0 -0
  81. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/template/measurement_template.py +0 -0
  82. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/template/parameter_template.py +0 -0
  83. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/template/process_template.py +0 -0
  84. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/template/property_template.py +0 -0
  85. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/util.py +0 -0
  86. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/valid_list.py +0 -0
  87. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/value/__init__.py +0 -0
  88. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/value/base_value.py +0 -0
  89. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/value/categorical_value.py +0 -0
  90. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/value/composition_value.py +0 -0
  91. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/value/continuous_value.py +0 -0
  92. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/value/discrete_categorical.py +0 -0
  93. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/value/empirical_formula.py +0 -0
  94. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/value/inchi_value.py +0 -0
  95. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/value/integer_value.py +0 -0
  96. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/value/molecular_value.py +0 -0
  97. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/value/nominal_categorical.py +0 -0
  98. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/value/nominal_composition.py +0 -0
  99. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/value/nominal_integer.py +0 -0
  100. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/value/nominal_real.py +0 -0
  101. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/value/normal_real.py +0 -0
  102. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/value/smiles_value.py +0 -0
  103. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/value/uniform_integer.py +0 -0
  104. {gemd-2.0.0 → gemd-2.1.3}/gemd/entity/value/uniform_real.py +0 -0
  105. {gemd-2.0.0 → gemd-2.1.3}/gemd/enumeration/__init__.py +0 -0
  106. {gemd-2.0.0 → gemd-2.1.3}/gemd/enumeration/base_enumeration.py +0 -0
  107. {gemd-2.0.0 → gemd-2.1.3}/gemd/enumeration/origin.py +0 -0
  108. {gemd-2.0.0 → gemd-2.1.3}/gemd/enumeration/sample_type.py +0 -0
  109. {gemd-2.0.0 → gemd-2.1.3}/gemd/json/__init__.py +0 -0
  110. {gemd-2.0.0 → gemd-2.1.3}/gemd/json/gemd_encoder.py +0 -0
  111. {gemd-2.0.0 → gemd-2.1.3}/gemd/json/gemd_json.py +0 -0
  112. {gemd-2.0.0 → gemd-2.1.3}/gemd/units/__init__.py +0 -0
  113. {gemd-2.0.0 → gemd-2.1.3}/gemd/units/citrine_en.txt +0 -0
  114. {gemd-2.0.0 → gemd-2.1.3}/gemd/units/constants_en.txt +0 -0
  115. {gemd-2.0.0 → gemd-2.1.3}/gemd/util/__init__.py +0 -0
  116. {gemd-2.0.0 → gemd-2.1.3}/gemd/util/impl.py +0 -0
  117. {gemd-2.0.0 → gemd-2.1.3}/gemd.egg-info/dependency_links.txt +0 -0
  118. {gemd-2.0.0 → gemd-2.1.3}/setup.cfg +0 -0
  119. {gemd-2.0.0 → gemd-2.1.3}/tests/test_examples.py +0 -0
@@ -1,9 +1,11 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: gemd
3
- Version: 2.0.0
3
+ Version: 2.1.3
4
4
  Summary: Python binding for Citrine's GEMD data model
5
5
  Home-page: http://github.com/CitrineInformatics/gemd-python
6
6
  Author: Citrine Informatics
7
+ License: UNKNOWN
8
+ Platform: UNKNOWN
7
9
  Classifier: Programming Language :: Python :: 3
8
10
  Classifier: Programming Language :: Python :: 3.8
9
11
  Classifier: Programming Language :: Python :: 3.9
@@ -11,13 +13,11 @@ Classifier: Programming Language :: Python :: 3.10
11
13
  Classifier: Programming Language :: Python :: 3.11
12
14
  Classifier: Programming Language :: Python :: 3.12
13
15
  Requires-Python: >=3.8
14
- License-File: LICENSE
15
- Requires-Dist: pint<0.24,>=0.20
16
- Requires-Dist: deprecation<3,>=2.1.0
16
+ Provides-Extra: scripts
17
17
  Provides-Extra: tests
18
- Requires-Dist: pytest<9,>=8.0.0; extra == "tests"
19
- Provides-Extra: tests-demo
20
- Requires-Dist: pandas<3,>=2.0.3; extra == "tests-demo"
21
- Provides-Extra: tests-entity-bounds
22
- Requires-Dist: numpy<2,>=1.24.4; extra == "tests-entity-bounds"
23
- Requires-Dist: pandas<3,>=2.0.3; extra == "tests-entity-bounds"
18
+ Provides-Extra: tests.demo
19
+ Provides-Extra: tests.entity.bounds
20
+ License-File: LICENSE
21
+
22
+ UNKNOWN
23
+
@@ -1,5 +1,5 @@
1
1
  # GEMD-python
2
- Python binding for Citrine's nextgen data model, GEMD.
2
+ Python binding for Citrine's data model, GEMD.
3
3
 
4
4
  This package provides a framework for storing information about the processes that create materials, the materials themselves, and measurements performed on those materials.
5
5
 
@@ -0,0 +1 @@
1
+ __version__ = "2.1.3"
@@ -1,4 +1,5 @@
1
1
  """Bake a cake."""
2
+ from importlib_resources import files
2
3
  from io import BytesIO
3
4
  import random
4
5
 
@@ -67,10 +68,7 @@ def get_template_scope():
67
68
 
68
69
  def import_toothpick_picture() -> BytesIO:
69
70
  """Return the stream of the toothpick picture."""
70
- from importlib.resources import read_binary
71
- resource = read_binary("gemd.demo", "toothpick.jpg")
72
-
73
- return BytesIO(resource)
71
+ return files("gemd.demo").joinpath("toothpick.jpg").open("rb")
74
72
 
75
73
 
76
74
  def make_cake_templates():
@@ -45,11 +45,10 @@ __all__ = [
45
45
 
46
46
  def import_table(filename=SMALL_TABLE):
47
47
  """Return the deserialized JSON table."""
48
- from importlib.resources import read_text
48
+ from importlib_resources import files
49
49
  import json
50
- table = json.loads(read_text("gemd.demo", filename))
51
50
 
52
- return table
51
+ return json.loads(files("gemd.demo").joinpath(filename).read_text())
53
52
 
54
53
 
55
54
  def _fingerprint(row):
@@ -1,13 +1,14 @@
1
1
  """Implementation of units."""
2
+ from deprecation import deprecated
2
3
  import functools
3
- from importlib.resources import read_text
4
+ from importlib_resources import files
4
5
  import os
5
6
  from pathlib import Path
6
7
  import re
7
8
  from tempfile import TemporaryDirectory
8
9
  from typing import Union, List, Tuple, Generator, Any
9
10
 
10
- from pint import UnitRegistry, Unit, register_unit_format
11
+ from pint import UnitRegistry, register_unit_format
11
12
  try: # Pint 0.23 migrated the location of this method, and augmented it
12
13
  from pint.pint_eval import tokenizer
13
14
  except ImportError: # pragma: no cover
@@ -27,18 +28,19 @@ __all__ = [
27
28
  ]
28
29
 
29
30
 
30
- def _deploy_default_files() -> str:
31
+ def _deploy_default_files() -> Tuple[Path, Path]:
31
32
  """Copy the units & constants file into a temporary directory."""
32
- units_path = Path(_TEMP_DIRECTORY.name) / "citrine_en.txt"
33
- units_path.write_text(read_text("gemd.units", "citrine_en.txt"), encoding="utf-8")
33
+ resources = files("gemd.units")
34
+ target_dir = Path(_TEMP_DIRECTORY.name)
35
+ target_paths = tuple(target_dir / f for f in ("citrine_en.txt", "constants_en.txt"))
36
+ for target in target_paths:
37
+ source = resources.joinpath(target.name)
38
+ target.write_text(source.read_text(), encoding="utf-8")
34
39
 
35
- constants_path = Path(_TEMP_DIRECTORY.name) / "constants_en.txt"
36
- constants_path.write_text(read_text("gemd.units", "constants_en.txt"), encoding="utf-8")
40
+ return target_paths
37
41
 
38
- return str(units_path)
39
42
 
40
-
41
- DEFAULT_FILE = _deploy_default_files()
43
+ DEFAULT_FILE, DEFAULT_CONSTANTS = _deploy_default_files()
42
44
  _ALLOWED_OPERATORS = {".", "+", "-", "*", "/", "//", "^", "**", "(", ")"}
43
45
 
44
46
 
@@ -131,6 +133,7 @@ def _scaling_identify_factors(
131
133
  """
132
134
  todo = []
133
135
  for block in blocks:
136
+ # Note: while Python does not recognize ^ as exponentiation, pint does
134
137
  i_exp = next((i for i, t in enumerate(block) if t.string in {"**", "^"}), len(block))
135
138
  i_name = next((i for i, t in enumerate(block) if t.type == NAME), None)
136
139
  numbers = [(i, t.string) for i, t in enumerate(block) if t.type == NUMBER and i < i_exp]
@@ -168,10 +171,14 @@ def _scaling_store_and_mangle(input_string: str, todo: List[Tuple[str, str, str]
168
171
  """
169
172
  for scaled_term, number_string, unit_string in todo:
170
173
  regex = rf"(?<![-+0-9.]){re.escape(scaled_term)}(?![0-9.])"
171
- stripped = re.sub(r"[+\s]+", "", scaled_term).replace("--", "")
174
+ stripped = re.sub(
175
+ r"(?<=\d)_(?=\d)", "", re.sub(r"[+\s]+", "", scaled_term).replace("--", "")
176
+ )
172
177
 
173
178
  if unit_string is not None:
174
- stripped_unit = re.sub(r"[+\s]+", "", unit_string).replace("--", "")
179
+ stripped_unit = re.sub(
180
+ r"(?<!0)(?=\.)", "0", re.sub(r"[+\s]+", "", unit_string)
181
+ ).replace("--", "")
175
182
  long_unit = f"{_REGISTRY.parse_units(stripped_unit)}"
176
183
  short_unit = f"{_REGISTRY.parse_units(stripped_unit):~}"
177
184
  long = stripped.replace(stripped_unit, "_" + long_unit)
@@ -201,7 +208,58 @@ def _scaling_preprocessor(input_string: str) -> str:
201
208
  return _scaling_store_and_mangle(input_string, todo)
202
209
 
203
210
 
204
- _REGISTRY: UnitRegistry = None # global requires it be defined in this scope
211
+ def _unmangle_scaling(input_string: str) -> str:
212
+ """Convert mangled scaling values into a pint-compatible expression."""
213
+ number_re = r'\b_(_)?(\d+)(_\d+)?([eE]_?\d+)?(_(?=[a-zA-Z]))?'
214
+ while match := re.search(number_re, input_string):
215
+ replacement = '' if match.group(1) is None else '-'
216
+ replacement += match.group(2)
217
+ replacement += '' if match.group(3) is None else match.group(3).replace('_', '.')
218
+ replacement += '' if match.group(4) is None else match.group(4).replace('_', '-')
219
+ replacement += '' if match.group(5) is None else match.group(5).replace('_', ' ')
220
+ input_string = input_string.replace(match.group(0), replacement)
221
+ return input_string
222
+
223
+
224
+ try: # pragma: no cover
225
+ # Pint 0.23 modified the preferred way to derive a custom class
226
+ # https://pint.readthedocs.io/en/0.23/advanced/custom-registry-class.html
227
+ from pint.registry import GenericUnitRegistry
228
+ from typing_extensions import TypeAlias
229
+
230
+ class _ScaleFactorUnit(UnitRegistry.Unit):
231
+ """Child class of Units for generating units w/ clean scaling factors."""
232
+
233
+ def __format__(self, format_spec):
234
+ result = super().__format__(format_spec)
235
+ return _unmangle_scaling(result)
236
+
237
+ class _ScaleFactorQuantity(UnitRegistry.Quantity):
238
+ """Child class of Quantity for generating units w/ clean scaling factors."""
239
+
240
+ pass
241
+
242
+ class _ScaleFactorRegistry(GenericUnitRegistry[_ScaleFactorQuantity, _ScaleFactorUnit]):
243
+ """UnitRegistry class that uses _GemdUnits."""
244
+
245
+ Quantity: TypeAlias = _ScaleFactorQuantity
246
+ Unit: TypeAlias = _ScaleFactorUnit
247
+
248
+ except ImportError: # pragma: no cover
249
+ # https://pint.readthedocs.io/en/0.21/advanced/custom-registry-class.html
250
+ class _ScaleFactorUnit(UnitRegistry.Unit):
251
+ """Child class of Units for generating units w/ clean scaling factors."""
252
+
253
+ def __format__(self, format_spec):
254
+ result = super().__format__(format_spec)
255
+ return _unmangle_scaling(result)
256
+
257
+ class _ScaleFactorRegistry(UnitRegistry):
258
+ """UnitRegistry class that uses _GemdUnits."""
259
+
260
+ _unit_class = _ScaleFactorUnit
261
+
262
+ _REGISTRY: _ScaleFactorRegistry = None # global requires it be defined in this scope
205
263
 
206
264
 
207
265
  @functools.lru_cache(maxsize=1024 * 1024)
@@ -244,38 +302,23 @@ def convert_units(value: float, starting_unit: str, final_unit: str) -> float:
244
302
 
245
303
 
246
304
  @register_unit_format("clean")
305
+ @deprecated(deprecated_in="2.1.0", removed_in="3.0.0", details="Scaling factor clean-up ")
247
306
  def _format_clean(unit, registry, **options):
248
- """Formatter that turns scaling-factor-units into numbers again."""
249
- numerator = []
250
- denominator = []
251
- for u, p in unit.items():
252
- if re.match(r"_[\d_]+", u):
253
- # Munged scaling factor; grab symbol, which is the prettier
254
- u = registry.get_symbol(u)
255
-
256
- if p == 1:
257
- numerator.append(u)
258
- elif p > 0:
259
- numerator.append(f"{u} ** {p}")
260
- elif p == -1:
261
- denominator.append(u)
262
- elif p < 0:
263
- denominator.append(f"{u} ** {-p}")
264
-
265
- if len(numerator) == 0:
266
- numerator = ["1"]
267
-
268
- if len(denominator) > 0:
269
- return " / ".join((" * ".join(numerator), " / ".join(denominator)))
270
- else:
271
- return " * ".join(numerator)
307
+ """
308
+ DEPRECATED Formatter that turns scaling-factor-units into numbers again.
309
+
310
+ Responsibility for this piece of clean-up has been shifted to a custom class.
311
+
312
+ """
313
+ from pint.formatting import _FORMATTERS
314
+ return _FORMATTERS["D"](unit, registry, **options)
272
315
 
273
316
 
274
317
  @functools.lru_cache(maxsize=1024)
275
- def parse_units(units: Union[str, Unit, None],
318
+ def parse_units(units: Union[str, UnitRegistry.Unit, None],
276
319
  *,
277
320
  return_unit: bool = False
278
- ) -> Union[str, Unit, None]:
321
+ ) -> Union[str, UnitRegistry.Unit, None]:
279
322
  """
280
323
  Parse a string or Unit into a standard string representation of the unit.
281
324
 
@@ -298,19 +341,20 @@ def parse_units(units: Union[str, Unit, None],
298
341
  else:
299
342
  return None
300
343
  elif isinstance(units, str):
301
- parsed = _REGISTRY.parse_units(units)
344
+ # SPT-1311 Protect against leaked mangled strings
345
+ parsed = _REGISTRY.parse_units(_unmangle_scaling(units))
302
346
  if return_unit:
303
347
  return parsed
304
348
  else:
305
- return f"{parsed:clean}"
306
- elif isinstance(units, Unit):
349
+ return f"{parsed}"
350
+ elif isinstance(units, UnitRegistry.Unit):
307
351
  return units
308
352
  else:
309
353
  raise UndefinedUnitError("Units must be given as a recognized unit string or Units object")
310
354
 
311
355
 
312
356
  @functools.lru_cache(maxsize=1024)
313
- def get_base_units(units: Union[str, Unit]) -> Tuple[Unit, float, float]:
357
+ def get_base_units(units: Union[str, UnitRegistry.Unit]) -> Tuple[UnitRegistry.Unit, float, float]:
314
358
  """
315
359
  Get the base units and conversion factors for the given unit.
316
360
 
@@ -350,21 +394,20 @@ def change_definitions_file(filename: str = None):
350
394
  if filename is None:
351
395
  target = DEFAULT_FILE
352
396
  else:
353
- # TODO: Handle case where user provides a units file but no constants file
354
397
  target = Path(filename).expanduser().resolve(strict=True)
355
398
 
356
399
  current_dir = Path.cwd()
357
400
  try:
358
- path = Path(target)
359
- os.chdir(path.parent)
360
- # Need to re-verify path because of some slippiness around tmp on MacOS
361
- _REGISTRY = UnitRegistry(filename=Path.cwd() / path.name,
362
- preprocessors=[_space_after_minus_preprocessor,
363
- _scientific_notation_preprocessor,
364
- _scaling_preprocessor
365
- ],
366
- autoconvert_offset_to_baseunit=True
367
- )
401
+ os.chdir(target.parent)
402
+ # Need to re-verify path because of some slippiness around tmp on macOS
403
+ updated = (Path.cwd() / target.name).resolve(strict=True)
404
+ _REGISTRY = _ScaleFactorRegistry(filename=updated,
405
+ preprocessors=[_space_after_minus_preprocessor,
406
+ _scientific_notation_preprocessor,
407
+ _scaling_preprocessor
408
+ ],
409
+ autoconvert_offset_to_baseunit=True
410
+ )
368
411
  finally:
369
412
  os.chdir(current_dir)
370
413
 
@@ -1,9 +1,11 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: gemd
3
- Version: 2.0.0
3
+ Version: 2.1.3
4
4
  Summary: Python binding for Citrine's GEMD data model
5
5
  Home-page: http://github.com/CitrineInformatics/gemd-python
6
6
  Author: Citrine Informatics
7
+ License: UNKNOWN
8
+ Platform: UNKNOWN
7
9
  Classifier: Programming Language :: Python :: 3
8
10
  Classifier: Programming Language :: Python :: 3.8
9
11
  Classifier: Programming Language :: Python :: 3.9
@@ -11,13 +13,11 @@ Classifier: Programming Language :: Python :: 3.10
11
13
  Classifier: Programming Language :: Python :: 3.11
12
14
  Classifier: Programming Language :: Python :: 3.12
13
15
  Requires-Python: >=3.8
14
- License-File: LICENSE
15
- Requires-Dist: pint<0.24,>=0.20
16
- Requires-Dist: deprecation<3,>=2.1.0
16
+ Provides-Extra: scripts
17
17
  Provides-Extra: tests
18
- Requires-Dist: pytest<9,>=8.0.0; extra == "tests"
19
- Provides-Extra: tests-demo
20
- Requires-Dist: pandas<3,>=2.0.3; extra == "tests-demo"
21
- Provides-Extra: tests-entity-bounds
22
- Requires-Dist: numpy<2,>=1.24.4; extra == "tests-entity-bounds"
23
- Requires-Dist: pandas<3,>=2.0.3; extra == "tests-entity-bounds"
18
+ Provides-Extra: tests.demo
19
+ Provides-Extra: tests.entity.bounds
20
+ License-File: LICENSE
21
+
22
+ UNKNOWN
23
+
@@ -108,4 +108,8 @@ gemd/units/constants_en.txt
108
108
  gemd/units/impl.py
109
109
  gemd/util/__init__.py
110
110
  gemd/util/impl.py
111
- tests/test_examples.py
111
+ tests/__init__.py
112
+ tests/test_examples.py
113
+ tests/units/__init__.py
114
+ tests/units/test_parser.py
115
+ tests/units/test_units.txt
@@ -1,5 +1,12 @@
1
- pint<0.24,>=0.20
2
1
  deprecation<3,>=2.1.0
2
+ importlib-resources<7,>=5.3
3
+ pint<0.24,>=0.20
4
+ typing_extensions<5,>=4.8
5
+
6
+ [scripts]
7
+ packagingsphinx==5.0.0
8
+ sphinx-rtd-theme==1.0.0
9
+ sphinxcontrib-apidoc==0.3.0
3
10
 
4
11
  [tests]
5
12
  pytest<9,>=8.0.0
@@ -0,0 +1,2 @@
1
+ gemd
2
+ tests
@@ -1,23 +1,20 @@
1
1
  from setuptools import setup, find_packages
2
- from os import path
3
- from packaging.version import Version
2
+ from pathlib import Path
4
3
  import re
5
4
 
6
5
  packages = find_packages()
7
6
 
8
- this_directory = path.abspath(path.dirname(__file__))
9
- version_file = path.join(this_directory, 'gemd', '__version__.py')
10
- version_re = r'''^__version__\s*=\s*(['"])([\w\.]+)\1$'''
11
- with open(version_file, 'r') as f:
12
- mo = re.search(version_re, f.read(), re.M)
13
- if mo:
14
- version = Version(mo.group(2))
15
- else:
16
- raise RuntimeError(f"Unable to find version string in {version_file}")
7
+ this_directory = Path(__file__).parent.absolute()
8
+ version_file = this_directory / 'gemd' / '__version__.py'
9
+ version_re = r'''^\s*__version__\s*=\s*(['"])([\w\.]+)\1$'''
10
+ if mo := re.search(version_re, version_file.read_text(), re.M):
11
+ version = mo.group(2)
12
+ else:
13
+ raise RuntimeError(f"Unable to find version string in {version_file}")
17
14
 
18
15
  setup(name='gemd',
19
16
  # Update this in gemd/__version__.py
20
- version=str(version),
17
+ version=version,
21
18
  python_requires='>=3.8',
22
19
  url='http://github.com/CitrineInformatics/gemd-python',
23
20
  description="Python binding for Citrine's GEMD data model",
@@ -37,9 +34,17 @@ setup(name='gemd',
37
34
  },
38
35
  install_requires=[
39
36
  "pint>=0.20,<0.24",
40
- "deprecation>=2.1.0,<3"
37
+ "deprecation>=2.1.0,<3",
38
+ "typing_extensions>=4.8,<5",
39
+ "importlib-resources>=5.3,<7"
41
40
  ],
42
41
  extras_require={
42
+ "scripts": [
43
+ "packaging"
44
+ "sphinx==5.0.0",
45
+ "sphinx-rtd-theme==1.0.0",
46
+ "sphinxcontrib-apidoc==0.3.0",
47
+ ],
43
48
  "tests": [
44
49
  "pytest>=8.0.0,<9"
45
50
  ],
File without changes
File without changes
@@ -0,0 +1,225 @@
1
+ from contextlib import contextmanager
2
+ from deprecation import DeprecatedWarning
3
+ from importlib_resources import files
4
+ import re
5
+ from pint import UnitRegistry
6
+ import pytest
7
+
8
+ from gemd.units import parse_units, convert_units, get_base_units, change_definitions_file, \
9
+ UndefinedUnitError, DefinitionSyntaxError, IncompatibleUnitsError
10
+
11
+
12
+ @pytest.mark.parametrize("return_unit", [True, False])
13
+ def test_parse_expected(return_unit):
14
+ """Test that we can parse the units that we expect to be able to."""
15
+ # Pint's parse_units actually gets this wrong
16
+ assert parse_units("m^-1 * newton / meter", return_unit=return_unit) == \
17
+ parse_units("N / m^2", return_unit=return_unit)
18
+
19
+ expected = [
20
+ "degC", "degF", "K",
21
+ "g", "kg", "mg", "ton",
22
+ "L", "mL",
23
+ "inch", "ft", "mm", "um",
24
+ "second", "ms", "hour", "minute", "ns",
25
+ "g/cm^3", "g/mL", "kg/cm^3",
26
+ "", "1",
27
+ "amu", # A line that was edited
28
+ "Seconds", # Added support for some title-case units
29
+ "delta_Celsius / hour", # Added to make sure pint version is right (>0.10)
30
+ "g / 2.5 cm", # Scaling factors are acceptable
31
+ "g / -+-25e-1 m", # Weird but fine
32
+ "ug / - -250 mL", # Spaces between unaries is acceptable
33
+ "1 / 10**5 degC", # Spaces between unaries is acceptable
34
+ "1 / 10_000 degC", # Spaces between unaries is acceptable
35
+ "m ** - 1", # Pint < 0.21 throws DefinitionSyntaxError
36
+ "gram / _10_minute", # Stringified Unit object SPT-1311
37
+ "gram / __1_2e_3minute", # Stringified Unit object SPT-1311
38
+ ]
39
+ for unit in expected:
40
+ parsed = parse_units(unit, return_unit=return_unit)
41
+ assert parsed == parse_units(parsed, return_unit=return_unit)
42
+ assert parse_units("") == 'dimensionless'
43
+ # Scaling factors bind tightly to trailing units
44
+ scaling = [
45
+ ("g / 2.5 cm", "g / (2.5 cm)"),
46
+ ("g / 2.5cm", "g / (2.5 cm)"),
47
+ ("g / 25.mm", "g / (25. mm)"),
48
+ ("g / 2.5 * cm", "g cm / 2.5")
49
+ ]
50
+ for left, right in scaling:
51
+ assert parse_units(left, return_unit=return_unit) == \
52
+ parse_units(right, return_unit=return_unit)
53
+
54
+
55
+ def test_parse_unexpected():
56
+ """Test that we cannot parse the units that we do not expect to."""
57
+ unexpected = [
58
+ "gibberish",
59
+ 5,
60
+ "cp", # Removed because of risk of collision with cP
61
+ "chain", # Survey units eliminated
62
+ "SECONDS", # Not just case insensitivity
63
+ "lb : in^3", # : is not a valid operator
64
+ ]
65
+ for unit in unexpected:
66
+ with pytest.raises(UndefinedUnitError):
67
+ parse_units(unit)
68
+
69
+ scaling = [
70
+ "3 rpm", # No leading digits
71
+ "16", # No values that are just integers
72
+ "16.2" # No values that are just floats
73
+ "g * 0/ m", # Zero scaling factor
74
+ "F * 1.1 3.5", # numeric syntax error
75
+ "F m 1.1", # scale follows unit
76
+ "F * 1.1 ** 2", # numeric syntax error
77
+ ]
78
+ for unit in scaling:
79
+ with pytest.raises(ValueError, match=r"[sS]caling"):
80
+ parse_units(unit)
81
+
82
+ definition = [
83
+ "/gram", # A leading operator makes no sense
84
+ ]
85
+ for unit in definition:
86
+ with pytest.raises(DefinitionSyntaxError):
87
+ parse_units(unit)
88
+
89
+ zero_division = [
90
+ "g / 0 m", # Division by zero
91
+ ]
92
+ for unit in zero_division:
93
+ with pytest.raises(ZeroDivisionError):
94
+ parse_units(unit)
95
+
96
+
97
+ def test_parse_none():
98
+ """Test that None parses as None."""
99
+ assert parse_units(None) is None
100
+ assert parse_units(None, return_unit=True) == parse_units("", return_unit=True)
101
+
102
+
103
+ def test_parse_units_as_units():
104
+ """Test that Units in == Units out."""
105
+ from gemd.units.impl import _REGISTRY
106
+
107
+ expected = [
108
+ _REGISTRY("kg").u,
109
+ ]
110
+ for unit in expected:
111
+ parsed = parse_units(unit)
112
+ assert parsed == parse_units(parsed)
113
+
114
+
115
+ def test_format():
116
+ """Test that custom formatting behaves as we hope."""
117
+ # use the default unit registry for now
118
+ reg = UnitRegistry()
119
+
120
+ result = parse_units("K^-2.0 m^-1e0 C^0 g^1 s^2")
121
+ assert "-" not in result
122
+ assert "[time]" in str(reg(result).dimensionality)
123
+ assert "[current]" not in str(reg(result).dimensionality)
124
+ kelvin = str(reg("K").units)
125
+ gram = str(reg("g").units)
126
+ second = str(reg("s").units)
127
+ assert kelvin in str(result)
128
+ assert gram in str(result)
129
+ assert second in str(result)
130
+ assert result.index(gram) < result.index(kelvin)
131
+ assert result.index(gram) < result.index(second)
132
+
133
+ assert not re.search(r"\d", parse_units("m kg / s"))
134
+ assert "/" not in parse_units("m kg s")
135
+ assert "1" not in parse_units("s")
136
+ assert "1" in parse_units("s^-1")
137
+ assert "2.5" in parse_units("g / 2.5 cm")
138
+
139
+
140
+ def test_conversion():
141
+ """Tests that check if particular units are interoperable."""
142
+ conversions = {"in_lb": "foot_pound"}
143
+ for source, dest in conversions.items():
144
+ assert convert_units(convert_units(1, source, dest), dest, source) == 1
145
+
146
+ # Verify that convert_units respects scaling factors
147
+ assert -1e-8 < convert_units(100, 'g / 100 mL', 'g/cc') - 1 < 1e-8
148
+ assert -1e-8 < convert_units(1, "g / 2.5 cm", "g / 25 mm") - 1 < 1e-8
149
+
150
+ # Verify that convert_units throws exceptions
151
+ with pytest.raises(IncompatibleUnitsError):
152
+ convert_units(1, 'mL', 'g')
153
+ with pytest.raises(IncompatibleUnitsError):
154
+ # https://pint.readthedocs.io/en/0.23/user/angular_frequency.html
155
+ convert_units(1, 'Hz', 'rpm')
156
+
157
+
158
+ def test_get_base_units():
159
+ """Test that base units & conversions make sense."""
160
+ from gemd.units.impl import _REGISTRY
161
+ assert get_base_units("degC") == (_REGISTRY("kelvin"), 1, 273.15)
162
+ assert get_base_units("degC") == get_base_units(_REGISTRY("degC"))
163
+ assert get_base_units("km") == (_REGISTRY("meter"), 1000, 0)
164
+ assert get_base_units("g / 25 mm") == (_REGISTRY("kg / m"), 0.04, 0)
165
+
166
+
167
+ @contextmanager
168
+ def _change_units(filename):
169
+ try:
170
+ change_definitions_file(filename)
171
+ yield
172
+ finally:
173
+ change_definitions_file()
174
+
175
+
176
+ def test_file_change(tmpdir):
177
+ """Test that swapping units files works."""
178
+ assert convert_units(1, 'm', 'cm') == 100
179
+ with pytest.raises(UndefinedUnitError):
180
+ assert convert_units(1, 'usd', 'USD') == 1
181
+
182
+ test_file = tmpdir / "test_units.txt"
183
+ test_file.write_binary(files("tests.units").joinpath("test_units.txt").read_bytes())
184
+ with _change_units(filename=test_file):
185
+ with pytest.raises(UndefinedUnitError):
186
+ assert convert_units(1, 'm', 'cm') == 100
187
+ assert convert_units(1, 'usd', 'USD') == 1
188
+ assert convert_units(1, 'm', 'cm') == 100 # And verify we're back to normal
189
+ with pytest.raises(UndefinedUnitError):
190
+ parse_units('mol : mol') # Ensure the preprocessor is still there
191
+
192
+
193
+ def test_punctuation():
194
+ """Test that punctuation parses reasonably."""
195
+ assert parse_units('mol.') == parse_units('moles')
196
+ assert parse_units('N.m') == parse_units('N * m')
197
+ with pytest.raises(UndefinedUnitError):
198
+ parse_units('mol : mol')
199
+
200
+
201
+ def test_exponents():
202
+ """SPT-874 fractional exponents were being treated as zero."""
203
+ megapascals = parse_units("MPa")
204
+ sqrt_megapascals = parse_units('MPa^0.5')
205
+ assert megapascals in sqrt_megapascals
206
+ assert sqrt_megapascals == parse_units(f"{megapascals} / {sqrt_megapascals}")
207
+ assert parse_units('MPa^1.5') == parse_units(f"{megapascals} * {sqrt_megapascals}")
208
+
209
+
210
+ def test__scientific_notation_preprocessor():
211
+ """Verify that numbers are converted into scientific notation."""
212
+ assert "1e2 kilogram" in parse_units("F* 10 ** 2 kg")
213
+ assert "1e2 kg" in f'{parse_units("F* 10 ** 2 kg", return_unit=True):~}'
214
+ assert "1e-5" in parse_units("F* mm*10**-5")
215
+ assert "1e" not in parse_units("F* kg * 10 cm")
216
+ assert "-3.07e2" in parse_units("F* -3.07 * 10 ** 2")
217
+ assert "11e2" in parse_units("F* 11*10^2")
218
+
219
+
220
+ def test_deprecation():
221
+ """Make sure deprecated things warn correctly."""
222
+ megapascals = parse_units("MPa", return_unit=True)
223
+ with pytest.warns(DeprecatedWarning):
224
+ stringified = f"{megapascals:clean}"
225
+ assert megapascals == parse_units(stringified, return_unit=True)
@@ -0,0 +1,4 @@
1
+
2
+ #### BASE UNITS ####
3
+
4
+ usd = [currency] = USD
@@ -1 +0,0 @@
1
- __version__ = "2.0.0"
@@ -1 +0,0 @@
1
- gemd
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes