halfedge 0.2.0__tar.gz → 0.4.0__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 (28) hide show
  1. {halfedge-0.2.0 → halfedge-0.4.0}/.github/workflows/pypi-project.yml +21 -21
  2. {halfedge-0.2.0 → halfedge-0.4.0}/.pre-commit-config.yaml +1 -0
  3. {halfedge-0.2.0/src/halfedge.egg-info → halfedge-0.4.0}/PKG-INFO +1 -1
  4. {halfedge-0.2.0 → halfedge-0.4.0}/pyproject.toml +2 -2
  5. {halfedge-0.2.0 → halfedge-0.4.0}/src/halfedge/__init__.py +10 -1
  6. {halfedge-0.2.0 → halfedge-0.4.0}/src/halfedge/half_edge_constructors.py +25 -1
  7. {halfedge-0.2.0 → halfedge-0.4.0}/src/halfedge/type_attrib.py +319 -2
  8. {halfedge-0.2.0 → halfedge-0.4.0/src/halfedge.egg-info}/PKG-INFO +1 -1
  9. {halfedge-0.2.0 → halfedge-0.4.0}/tests/test_classes.py +31 -0
  10. {halfedge-0.2.0 → halfedge-0.4.0}/tests/test_constructors.py +0 -7
  11. {halfedge-0.2.0 → halfedge-0.4.0}/.gitignore +0 -0
  12. {halfedge-0.2.0 → halfedge-0.4.0}/README.md +0 -0
  13. {halfedge-0.2.0 → halfedge-0.4.0}/setup.cfg +0 -0
  14. {halfedge-0.2.0 → halfedge-0.4.0}/src/halfedge/half_edge_elements.py +0 -0
  15. {halfedge-0.2.0 → halfedge-0.4.0}/src/halfedge/half_edge_object.py +0 -0
  16. {halfedge-0.2.0 → halfedge-0.4.0}/src/halfedge/half_edge_querries.py +0 -0
  17. {halfedge-0.2.0 → halfedge-0.4.0}/src/halfedge/py.typed +0 -0
  18. {halfedge-0.2.0 → halfedge-0.4.0}/src/halfedge/validations.py +0 -0
  19. {halfedge-0.2.0 → halfedge-0.4.0}/src/halfedge.egg-info/SOURCES.txt +0 -0
  20. {halfedge-0.2.0 → halfedge-0.4.0}/src/halfedge.egg-info/dependency_links.txt +0 -0
  21. {halfedge-0.2.0 → halfedge-0.4.0}/src/halfedge.egg-info/requires.txt +0 -0
  22. {halfedge-0.2.0 → halfedge-0.4.0}/src/halfedge.egg-info/top_level.txt +0 -0
  23. {halfedge-0.2.0 → halfedge-0.4.0}/tests/__init__.py +0 -0
  24. {halfedge-0.2.0 → halfedge-0.4.0}/tests/conftest.py +0 -0
  25. {halfedge-0.2.0 → halfedge-0.4.0}/tests/test_elements.py +0 -0
  26. {halfedge-0.2.0 → halfedge-0.4.0}/tests/test_object_pickups.py +0 -0
  27. {halfedge-0.2.0 → halfedge-0.4.0}/tests/test_operations.py +0 -0
  28. {halfedge-0.2.0 → halfedge-0.4.0}/tests/test_validations.py +0 -0
@@ -38,27 +38,27 @@ jobs:
38
38
  run: |
39
39
  pytest
40
40
 
41
- # If the tests pass, try to bump the version number. If no bump is warranted,
42
- # pass silently.
43
- bump_version:
44
- runs-on: ubuntu-latest
45
- name: "Bump version and create changelog with commitizen"
46
- continue-on-error: true
47
- needs: [tests]
48
- if: github.ref == 'refs/heads/dev'
49
- steps:
50
- - name: Check out
51
- uses: actions/checkout@v4
52
- with:
53
- fetch-depth: 0
54
- token: "${{ secrets.COMMITIZEN_BUMP }}"
55
- - id: cz
56
- name: Create bump and changelog
57
- uses: commitizen-tools/commitizen-action@master
58
- with:
59
- github_token: ${{ secrets.COMMITIZEN_BUMP }}
60
- - name: Print Version
61
- run: echo "Bumped to version ${{ steps.cz.outputs.version }}"
41
+ # # If the tests pass, try to bump the version number. If no bump is warranted,
42
+ # # pass silently.
43
+ # bump_version:
44
+ # runs-on: ubuntu-latest
45
+ # name: "Bump version and create changelog with commitizen"
46
+ # continue-on-error: true
47
+ # needs: [tests]
48
+ # if: github.ref == 'refs/heads/dev'
49
+ # steps:
50
+ # - name: Check out
51
+ # uses: actions/checkout@v4
52
+ # with:
53
+ # fetch-depth: 0
54
+ # token: "${{ secrets.COMMITIZEN_BUMP }}"
55
+ # - id: cz
56
+ # name: Create bump and changelog
57
+ # uses: commitizen-tools/commitizen-action@master
58
+ # with:
59
+ # github_token: ${{ secrets.COMMITIZEN_BUMP }}
60
+ # - name: Print Version
61
+ # run: echo "Bumped to version ${{ steps.cz.outputs.version }}"
62
62
 
63
63
  # Deploy on test.pypi when branch is dev and commit message starts with 'bump'
64
64
  deploy-on-testpypi:
@@ -110,6 +110,7 @@ repos:
110
110
  - --disable=useless-return # conflicts with mypy
111
111
  - --disable=assignment-from-no-return # prevents overloading Attrib methods to only raise an Exception
112
112
  - --disable=useless-parent-delegation
113
+ - --disable=R0801 # Similar lines in multiple files
113
114
  - --load-plugins=pylint.extensions.docparams
114
115
  - --accept-no-param-doc=n
115
116
  - --accept-no-raise-doc=n
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: halfedge
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: A typical half-edge data structure with some padding
5
5
  Author-email: Shay Hill <shay_public@hotmail.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "halfedge"
3
- version = "0.2.0"
3
+ version = "0.4.0"
4
4
  description = "A typical half-edge data structure with some padding"
5
5
  authors = [{ name = "Shay Hill", email = "shay_public@hotmail.com" }]
6
6
  license = {text = "MIT"}
@@ -25,7 +25,7 @@ build-backend = "setuptools.build_meta"
25
25
 
26
26
  [tool.commitizen]
27
27
  name = "cz_conventional_commits"
28
- version = "0.2.0"
28
+ version = "0.4.0"
29
29
  tag_format = "$version"
30
30
  major-version-zero = true
31
31
  version_files = ["pyproject.toml:^version"]
@@ -1,25 +1,34 @@
1
1
  """Allow modules to be imported from top-level."""
2
2
 
3
- from halfedge.half_edge_elements import Edge, Face, Vert
3
+ from halfedge.half_edge_constructors import BlindHalfEdges
4
+ from halfedge.half_edge_elements import Edge, Face, MeshElementBase, Vert
4
5
  from halfedge.half_edge_object import HalfEdges
5
6
  from halfedge.type_attrib import (
6
7
  Attrib,
7
8
  ContagionAttrib,
9
+ EdgeAttrib,
10
+ FaceAttrib,
8
11
  IncompatibleAttrib,
9
12
  NumericAttrib,
10
13
  Vector2Attrib,
11
14
  Vector3Attrib,
15
+ VertAttrib,
12
16
  )
13
17
 
14
18
  __all__ = [
15
19
  "Attrib",
20
+ "BlindHalfEdges",
16
21
  "ContagionAttrib",
17
22
  "Edge",
23
+ "EdgeAttrib",
18
24
  "Face",
25
+ "FaceAttrib",
19
26
  "HalfEdges",
20
27
  "IncompatibleAttrib",
28
+ "MeshElementBase",
21
29
  "NumericAttrib",
22
30
  "Vector2Attrib",
23
31
  "Vector3Attrib",
24
32
  "Vert",
33
+ "VertAttrib",
25
34
  ]
@@ -30,7 +30,10 @@ from paragraphs import par
30
30
  from halfedge.half_edge_elements import Edge, Face, ManifoldMeshError, Vert
31
31
 
32
32
  if TYPE_CHECKING:
33
- from halfedge.type_attrib import Attrib
33
+ from halfedge.type_attrib import Attrib, StaticAttrib
34
+
35
+
36
+ _T = TypeVar("_T")
34
37
 
35
38
  _TBlindHalfEdges = TypeVar("_TBlindHalfEdges", bound="BlindHalfEdges")
36
39
 
@@ -44,6 +47,27 @@ class BlindHalfEdges:
44
47
  self.edges: set[Edge] = set()
45
48
  else:
46
49
  self.edges = edges
50
+ self.attrib: dict[str, StaticAttrib[Any]] = {}
51
+
52
+ def set_attrib(self, attrib: StaticAttrib[Any]) -> None:
53
+ """Set an attribute.
54
+
55
+ :param attrib: StaticAttrib instance
56
+ """
57
+ self.attrib[type(attrib).__name__] = attrib.copy_to_element(self)
58
+
59
+ def get_attrib(self, attrib: type[StaticAttrib[_T]]) -> StaticAttrib[_T]:
60
+ """Get a StaticAttrib.
61
+
62
+ :param attrib: StaticAttrib class
63
+ :returns: StaticAttrib instance
64
+ :raise AttributeError: if StaticAttrib not found in self.attrib
65
+ """
66
+ try:
67
+ return self.attrib[attrib.__name__]
68
+ except KeyError as e:
69
+ msg = f"{attrib.__name__} not found in {self.__class__.__name__}"
70
+ raise AttributeError(msg) from e
47
71
 
48
72
  def new_vert(self, *attributes: Attrib[Any], edge: Edge | None = None) -> Vert:
49
73
  """Create a new Vert instance.
@@ -66,11 +66,106 @@ from typing import TYPE_CHECKING, Any, Generic, Literal, Tuple, TypeVar
66
66
  from paragraphs import par
67
67
 
68
68
  if TYPE_CHECKING:
69
- from halfedge.half_edge_elements import MeshElementBase
69
+ from halfedge.half_edge_constructors import BlindHalfEdges
70
+ from halfedge.half_edge_elements import Edge, Face, MeshElementBase, Vert
70
71
 
71
72
  _T = TypeVar("_T")
72
73
 
73
74
 
75
+ class StaticAttrib(Generic[_T]):
76
+ """Base class for storing a, potentially inferred, attribute value.
77
+
78
+ This is the equivalent to the Attrib class, but for meshes, which will never be
79
+ merged or split.
80
+ """
81
+
82
+ __slots__ = ("_value", "mesh")
83
+
84
+ def __new__(
85
+ cls: type[_TStaticAttrib],
86
+ value: _T | None = None,
87
+ mesh: BlindHalfEdges | None = None,
88
+ ) -> _TStaticAttrib:
89
+ """Raise an exception if the attribute is not subclassed."""
90
+ del value
91
+ del mesh
92
+ if cls is StaticAttrib:
93
+ msg = "StaticAttrib is an abstract class and cannot be instantiated."
94
+ raise TypeError(msg)
95
+ return object.__new__(cls)
96
+
97
+ def __init__(
98
+ self, value: _T | None = None, mesh: BlindHalfEdges | None = None
99
+ ) -> None:
100
+ """Set value and mesh."""
101
+ self._value = value
102
+ self.mesh = mesh
103
+
104
+ def copy_to_element(
105
+ self: StaticAttrib[_T], mesh: BlindHalfEdges
106
+ ) -> StaticAttrib[_T]:
107
+ """Return a new instance with the same value, assigned to a new mesh.
108
+
109
+ :param mesh: BlindHalfEdges instance to which attrib will be assigned.
110
+ :return: Attrib instance
111
+ """
112
+ return type(self)(self._value, mesh)
113
+
114
+ @property
115
+ def value(self) -> _T:
116
+ """Return value if set, else try to infer a value.
117
+
118
+ :return: Value of the attribute
119
+ :raises AttributeError: If no value is set and _infer_value fails
120
+ """
121
+ if self._value is not None:
122
+ return self._value
123
+ with suppress(NotImplementedError, ValueError):
124
+ value = self._infer_value()
125
+ self._value = value
126
+ return self._value
127
+ msg = "no value set and failed to infer from 'self.mesh'"
128
+ raise AttributeError(msg)
129
+
130
+ def _infer_value(self) -> _T:
131
+ """Get value of self from self._mesh.
132
+
133
+ Use the containing mesh to determine a value for self. If no value can be
134
+ determined, return None.
135
+
136
+ The purpose is to allow lazy attributes like edge norm and face area. Use
137
+ caution, however. These need to be calculated before merging since the method
138
+ may not support the new shape. For instance, this method might calculate the
139
+ area of a triangle, but would fail if two triangles were merged into a
140
+ square. To keep this safe, the _value is calculated *before* any merging. In
141
+ the "area of a triangle" example,
142
+
143
+ * The area calculation is deferred until the first merge.
144
+ * At the first merge, the area of each merged triangle is calculated. The
145
+ implication here is that calculation *cannot* be deferred till after a
146
+ merge.
147
+ * The merged method sums areas of the merged triangles at the first and
148
+ subsequent mergers, so further triangle area calculations (which
149
+ wouldn't work on the merged shapes anyway) are not required.
150
+
151
+ If you infer a value, cache it by setting self._value.
152
+
153
+ If you do not intend to infer values, raise an exception. This exception
154
+ should occur *before* an AttributeError is raised for a potentially missing
155
+ mesh attribute. It should be clear that _infer_value failed because there
156
+ is no provision for inferring this Attrib.value, *not* because the
157
+ user failed to set the Attrib property attribute.
158
+ """
159
+ msg = par(
160
+ f"""'{type(self).__name__}' has no provision for inferring a value from
161
+ 'self.mesh'"""
162
+ )
163
+ raise AttributeError(msg)
164
+
165
+
166
+ _TStaticAttrib = TypeVar("_TStaticAttrib", bound=StaticAttrib[Any])
167
+
168
+
74
169
  class Attrib(Generic[_T]):
75
170
  """Base class for element attributes.
76
171
 
@@ -121,7 +216,7 @@ class Attrib(Generic[_T]):
121
216
  value = self._infer_value()
122
217
  self._value = value
123
218
  return self._value
124
- msg = f"no value set and failed to infer from {self.element}"
219
+ msg = "no value set and failed to infer from 'self.element'"
125
220
  raise AttributeError(msg)
126
221
 
127
222
  def copy_to_element(self: Attrib[_T], element: MeshElementBase) -> Attrib[_T]:
@@ -217,6 +312,228 @@ class Attrib(Generic[_T]):
217
312
  _TAttrib = TypeVar("_TAttrib", bound=Attrib[Any])
218
313
 
219
314
 
315
+ class VertAttrib(Generic[_T]):
316
+ """Base class for Vert attributes."""
317
+
318
+ __slots__ = ("_value", "vert")
319
+
320
+ def __new__(
321
+ cls: type[_TVertAttrib], value: _T | None = None, vert: Vert | None = None
322
+ ) -> _TVertAttrib:
323
+ """Raise an exception if the attribute is not subclassed."""
324
+ del value
325
+ del vert
326
+ if cls is VertAttrib:
327
+ msg = "VertAttrib is an abstract class and cannot be instantiated."
328
+ raise TypeError(msg)
329
+ return object.__new__(cls)
330
+
331
+ def __init__(self, value: _T | None = None, vert: Vert | None = None) -> None:
332
+ """Set value and vert."""
333
+ self._value = value
334
+ self.vert = vert
335
+
336
+ @property
337
+ def value(self) -> _T:
338
+ """Return value if set, else try to infer a value.
339
+
340
+ :return: Value of the attribute
341
+ :raises AttributeError: If no value is set and _infer_value fails
342
+ """
343
+ if self._value is not None:
344
+ return self._value
345
+ with suppress(NotImplementedError, ValueError):
346
+ value = self._infer_value()
347
+ self._value = value
348
+ return self._value
349
+ msg = "no value set and failed to infer from 'self.vert'"
350
+ raise AttributeError(msg)
351
+
352
+ def copy_to_vert(self: VertAttrib[_T], vert: Vert) -> VertAttrib[_T]:
353
+ """Return a new instance with the same value, assigned to a new vert.
354
+
355
+ :param vert: New vert
356
+ :return: VertAttrib instance
357
+ """
358
+ return type(self)(self._value, vert)
359
+
360
+ @classmethod
361
+ def merge(cls, *merge_from: _TVertAttrib | None) -> _TVertAttrib | None:
362
+ """Get value of self from self._merge_from.
363
+
364
+ :param merge_from: VertAttrib instances to merge (all of the same class)
365
+ :return: VertAttrib instance or None
366
+ """
367
+ _ = merge_from
368
+ return None
369
+
370
+ def split(self: _TVertAttrib) -> _TVertAttrib | None:
371
+ """Define how attribute will be passed when dividing self.vert.
372
+
373
+ :return: VertAttrib instance or None
374
+ """
375
+ return None
376
+
377
+ def _infer_value(self) -> _T:
378
+ """Get value of self from self._vert."""
379
+ msg = par(
380
+ f"""'{type(self).__name__}' has no provision for inferring a value from
381
+ 'self.vert'"""
382
+ )
383
+ raise AttributeError(msg)
384
+
385
+
386
+ _TVertAttrib = TypeVar("_TVertAttrib", bound=VertAttrib[Any])
387
+
388
+
389
+ class EdgeAttrib(Generic[_T]):
390
+ """Base class for Edge attributes."""
391
+
392
+ __slots__ = ("_value", "edge")
393
+
394
+ def __new__(
395
+ cls: type[_TEdgeAttrib], value: _T | None = None, edge: Edge | None = None
396
+ ) -> _TEdgeAttrib:
397
+ """Raise an exception if the attribute is not subclassed."""
398
+ del value
399
+ del edge
400
+ if cls is EdgeAttrib:
401
+ msg = "EdgeAttrib is an abstract class and cannot be instantiated."
402
+ raise TypeError(msg)
403
+ return object.__new__(cls)
404
+
405
+ def __init__(self, value: _T | None = None, edge: Edge | None = None) -> None:
406
+ """Set value and edge."""
407
+ self._value = value
408
+ self.edge = edge
409
+
410
+ @property
411
+ def value(self) -> _T:
412
+ """Return value if set, else try to infer a value.
413
+
414
+ :return: Value of the attribute
415
+ :raises AttributeError: If no value is set and _infer_value fails
416
+ """
417
+ if self._value is not None:
418
+ return self._value
419
+ with suppress(NotImplementedError, ValueError):
420
+ value = self._infer_value()
421
+ self._value = value
422
+ return self._value
423
+ msg = "no value set and failed to infer from 'self.edge'"
424
+ raise AttributeError(msg)
425
+
426
+ def copy_to_edge(self: EdgeAttrib[_T], edge: Edge) -> EdgeAttrib[_T]:
427
+ """Return a new instance with the same value, assigned to a new edge.
428
+
429
+ :param edge: New edge
430
+ :return: EdgeAttrib instance
431
+ """
432
+ return type(self)(self._value, edge)
433
+
434
+ @classmethod
435
+ def merge(cls, *merge_from: _TEdgeAttrib | None) -> _TEdgeAttrib | None:
436
+ """Get value of self from self._merge_from.
437
+
438
+ :param merge_from: EdgeAttrib instances to merge (all of the same class)
439
+ :return: EdgeAttrib instance or None
440
+ """
441
+ _ = merge_from
442
+ return None
443
+
444
+ def split(self: _TEdgeAttrib) -> _TEdgeAttrib | None:
445
+ """Define how attribute will be passed when dividing self.edge.
446
+
447
+ :return: EdgeAttrib instance or None
448
+ """
449
+ return None
450
+
451
+ def _infer_value(self) -> _T:
452
+ """Get value of self from self._edge."""
453
+ msg = par(
454
+ f"""'{type(self).__name__}' has no provision for inferring a value from
455
+ 'self.edge'"""
456
+ )
457
+ raise AttributeError(msg)
458
+
459
+
460
+ _TEdgeAttrib = TypeVar("_TEdgeAttrib", bound=EdgeAttrib[Any])
461
+
462
+
463
+ class FaceAttrib(Generic[_T]):
464
+ """Base class for Face attributes."""
465
+
466
+ __slots__ = ("_value", "face")
467
+
468
+ def __new__(
469
+ cls: type[_TFaceAttrib], value: _T | None = None, face: Face | None = None
470
+ ) -> _TFaceAttrib:
471
+ """Raise an exception if the attribute is not subclassed."""
472
+ del value
473
+ del face
474
+ if cls is FaceAttrib:
475
+ msg = "FaceAttrib is an abstract class and cannot be instantiated."
476
+ raise TypeError(msg)
477
+ return object.__new__(cls)
478
+
479
+ def __init__(self, value: _T | None = None, face: Face | None = None) -> None:
480
+ """Set value and face."""
481
+ self._value = value
482
+ self.face = face
483
+
484
+ @property
485
+ def value(self) -> _T:
486
+ """Return value if set, else try to infer a value.
487
+
488
+ :return: Value of the attribute
489
+ :raises AttributeError: If no value is set and _infer_value fails
490
+ """
491
+ if self._value is not None:
492
+ return self._value
493
+ with suppress(NotImplementedError, ValueError):
494
+ value = self._infer_value()
495
+ self._value = value
496
+ return self._value
497
+ msg = "no value set and failed to infer from 'self.face'"
498
+ raise AttributeError(msg)
499
+
500
+ def copy_to_face(self: FaceAttrib[_T], face: Face) -> FaceAttrib[_T]:
501
+ """Return a new instance with the same value, assigned to a new face.
502
+
503
+ :param face: New face
504
+ :return: FaceAttrib instance
505
+ """
506
+ return type(self)(self._value, face)
507
+
508
+ @classmethod
509
+ def merge(cls, *merge_from: _TFaceAttrib | None) -> _TFaceAttrib | None:
510
+ """Get value of self from self._merge_from.
511
+
512
+ :param merge_from: FaceAttrib instances to merge (all of the same class)
513
+ :return: FaceAttrib instance or None
514
+ """
515
+ _ = merge_from
516
+ return None
517
+
518
+ def split(self: _TFaceAttrib) -> _TFaceAttrib | None:
519
+ """Define how attribute will be passed when dividing self.face.
520
+
521
+ :return: FaceAttrib instance or None
522
+ """
523
+ return None
524
+
525
+ def _infer_value(self) -> _T:
526
+ """Get value of self from self._face."""
527
+ msg = par(
528
+ f"""'{type(self).__name__}' has no provision for inferring a value from
529
+ 'self.face'"""
530
+ )
531
+ raise AttributeError(msg)
532
+
533
+
534
+ _TFaceAttrib = TypeVar("_TFaceAttrib", bound=FaceAttrib[Any])
535
+
536
+
220
537
  class ContagionAttrib(Attrib[Literal[True]]):
221
538
  """Spread value when combining with anything.
222
539
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: halfedge
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: A typical half-edge data structure with some padding
5
5
  Author-email: Shay Hill <shay_public@hotmail.com>
6
6
  License: MIT
@@ -12,6 +12,7 @@ from typing import Any, Tuple, TypeVar
12
12
 
13
13
  import pytest
14
14
 
15
+ from halfedge.half_edge_constructors import BlindHalfEdges
15
16
  from halfedge.half_edge_elements import (
16
17
  Edge,
17
18
  Face,
@@ -27,6 +28,7 @@ from halfedge.type_attrib import (
27
28
  ContagionAttrib,
28
29
  IncompatibleAttrib,
29
30
  NumericAttrib,
31
+ StaticAttrib,
30
32
  Vector2Attrib,
31
33
  Vector3Attrib,
32
34
  )
@@ -50,6 +52,10 @@ class MyAttrib(Attrib[int]):
50
52
  """An attribute with an integer value."""
51
53
 
52
54
 
55
+ class MyStaticAttrib(StaticAttrib[int]):
56
+ """A static attribute with an integer value."""
57
+
58
+
53
59
  class TestCannotInstantiateAbstractClasses:
54
60
  def test_cannot_instantiate_abstract_class(self) -> None:
55
61
  """Raise TypeError when instantiating an abstract class."""
@@ -87,6 +93,31 @@ class TestCannotInstantiateAbstractClasses:
87
93
  _ = Vector3Attrib()
88
94
  assert "cannot be instantiated" in err.value.args[0]
89
95
 
96
+ def test_cannot_instantiate_static_attrib(self) -> None:
97
+ """Raise TypeError when instantiating an abstract class."""
98
+ with pytest.raises(TypeError) as err:
99
+ _: StaticAttrib[Any] = StaticAttrib()
100
+ assert "cannot be instantiated" in err.value.args[0]
101
+
102
+
103
+ class TestStaticAttrib:
104
+ def test_attribute_error_if_no_value_set(self) -> None:
105
+ """Raise AttributeError if no value set."""
106
+ attrib = MyStaticAttrib()
107
+ with pytest.raises(AttributeError):
108
+ _ = attrib.value
109
+
110
+
111
+ class TestBlindHalfEdgesAttribSettersAndGetters:
112
+ def test_set_attrib(self) -> None:
113
+ """Set an attrib by passing a MeshElementBase instance"""
114
+ mesh = BlindHalfEdges()
115
+ attrib = MyStaticAttrib(7)
116
+ mesh.set_attrib(attrib)
117
+ stored_attrib = mesh.get_attrib(MyStaticAttrib)
118
+ assert stored_attrib.value == 7
119
+ assert stored_attrib.mesh is mesh
120
+
90
121
 
91
122
  class TestAttribBaseClass:
92
123
  def test_attribute_error_if_no_value_set(self) -> None:
@@ -5,8 +5,6 @@ created: 170204 14:22:23
5
5
 
6
6
  # pyright: reportPrivateUsage=false
7
7
 
8
- import itertools
9
- import random
10
8
  from typing import Any, Dict, Tuple
11
9
 
12
10
  import pytest
@@ -22,11 +20,6 @@ from halfedge.half_edge_querries import StaticHalfEdges
22
20
  from halfedge.type_attrib import IncompatibleAttrib, NumericAttrib
23
21
  from tests.conftest import get_canonical_mesh
24
22
 
25
- alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
26
- identifiers = (
27
- "".join(random.choice(alphabet) for _ in range(10)) for _ in itertools.count()
28
- )
29
-
30
23
 
31
24
  class Coordinate(IncompatibleAttrib[Tuple[float, ...]]):
32
25
  pass
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes