b3dkit 0.4.0__tar.gz → 0.5.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 (71) hide show
  1. b3dkit-0.5.0/.coveragerc +20 -0
  2. {b3dkit-0.4.0 → b3dkit-0.5.0}/CHANGELOG.md +37 -1
  3. {b3dkit-0.4.0 → b3dkit-0.5.0}/PKG-INFO +1 -1
  4. {b3dkit-0.4.0 → b3dkit-0.5.0}/pyproject.toml +10 -3
  5. {b3dkit-0.4.0 → b3dkit-0.5.0}/src/b3dkit/bolt_fittings.py +3 -3
  6. {b3dkit-0.4.0 → b3dkit-0.5.0}/src/b3dkit/dovetail.py +23 -31
  7. {b3dkit-0.4.0 → b3dkit-0.5.0}/tests/test_antichamfer.py +4 -19
  8. {b3dkit-0.4.0 → b3dkit-0.5.0}/tests/test_ball_socket.py +4 -13
  9. {b3dkit-0.4.0 → b3dkit-0.5.0}/tests/test_basic_shapes.py +0 -15
  10. {b3dkit-0.4.0 → b3dkit-0.5.0}/tests/test_bolt_fittings.py +47 -20
  11. b3dkit-0.5.0/tests/test_click_fit.py +19 -0
  12. {b3dkit-0.4.0 → b3dkit-0.5.0}/tests/test_dovetail.py +137 -25
  13. {b3dkit-0.4.0 → b3dkit-0.5.0}/tests/test_hexwall.py +10 -19
  14. {b3dkit-0.4.0 → b3dkit-0.5.0}/tests/test_high_top_slide_box.py +0 -18
  15. {b3dkit-0.4.0 → b3dkit-0.5.0}/tests/test_slide_box.py +10 -21
  16. {b3dkit-0.4.0 → b3dkit-0.5.0}/tests/test_twist_snap.py +10 -19
  17. b3dkit-0.4.0/.coveragerc +0 -6
  18. b3dkit-0.4.0/tests/conftest.py +0 -12
  19. b3dkit-0.4.0/tests/test_click_fit.py +0 -32
  20. {b3dkit-0.4.0 → b3dkit-0.5.0}/.github/workflows/release.yml +0 -0
  21. {b3dkit-0.4.0 → b3dkit-0.5.0}/.github/workflows/test.yml +0 -0
  22. {b3dkit-0.4.0 → b3dkit-0.5.0}/.gitignore +0 -0
  23. {b3dkit-0.4.0 → b3dkit-0.5.0}/.pre-commit-config.yaml +0 -0
  24. {b3dkit-0.4.0 → b3dkit-0.5.0}/.vscode/settings.json +0 -0
  25. {b3dkit-0.4.0 → b3dkit-0.5.0}/CONTRIBUTING.md +0 -0
  26. {b3dkit-0.4.0 → b3dkit-0.5.0}/LICENSE +0 -0
  27. {b3dkit-0.4.0 → b3dkit-0.5.0}/README.md +0 -0
  28. {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/CONTRIBUTING.md +0 -0
  29. {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/antichamfer.md +0 -0
  30. {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/ball_socket.md +0 -0
  31. {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/basic_shapes.md +0 -0
  32. {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/bolt_fittings.md +0 -0
  33. {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/click_fit.md +0 -0
  34. {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/dovetail.md +0 -0
  35. {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/dovetail.png +0 -0
  36. {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/hexwall.md +0 -0
  37. {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/high_top_slide_box.md +0 -0
  38. {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/index.md +0 -0
  39. {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/point.md +0 -0
  40. {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/reference/antichamfer.md +0 -0
  41. {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/reference/ball_socket.md +0 -0
  42. {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/reference/basic_shapes.md +0 -0
  43. {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/reference/bolt_fittings.md +0 -0
  44. {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/reference/click_fit.md +0 -0
  45. {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/reference/dovetail.md +0 -0
  46. {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/reference/hexwall.md +0 -0
  47. {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/reference/high_top_slide_box.md +0 -0
  48. {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/reference/index.md +0 -0
  49. {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/reference/point.md +0 -0
  50. {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/reference/slide_box.md +0 -0
  51. {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/reference/twist_snap.md +0 -0
  52. {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/requirements.txt +0 -0
  53. {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/slide_box.md +0 -0
  54. {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/twist_snap.md +0 -0
  55. {b3dkit-0.4.0 → b3dkit-0.5.0}/docs/twist_snap.png +0 -0
  56. {b3dkit-0.4.0 → b3dkit-0.5.0}/mkdocs.yml +0 -0
  57. {b3dkit-0.4.0 → b3dkit-0.5.0}/readthedocs.yaml +0 -0
  58. {b3dkit-0.4.0 → b3dkit-0.5.0}/src/b3dkit/__init__.py +0 -0
  59. {b3dkit-0.4.0 → b3dkit-0.5.0}/src/b3dkit/antichamfer.py +0 -0
  60. {b3dkit-0.4.0 → b3dkit-0.5.0}/src/b3dkit/ball_socket.py +0 -0
  61. {b3dkit-0.4.0 → b3dkit-0.5.0}/src/b3dkit/basic_shapes.py +0 -0
  62. {b3dkit-0.4.0 → b3dkit-0.5.0}/src/b3dkit/click_fit.py +0 -0
  63. {b3dkit-0.4.0 → b3dkit-0.5.0}/src/b3dkit/hexwall.py +0 -0
  64. {b3dkit-0.4.0 → b3dkit-0.5.0}/src/b3dkit/high_top_slide_box.py +0 -0
  65. {b3dkit-0.4.0 → b3dkit-0.5.0}/src/b3dkit/point.py +0 -0
  66. {b3dkit-0.4.0 → b3dkit-0.5.0}/src/b3dkit/py.typed +0 -0
  67. {b3dkit-0.4.0 → b3dkit-0.5.0}/src/b3dkit/slide_box.py +0 -0
  68. {b3dkit-0.4.0 → b3dkit-0.5.0}/src/b3dkit/twist_snap.py +0 -0
  69. {b3dkit-0.4.0 → b3dkit-0.5.0}/tests/test_docs.py +0 -0
  70. {b3dkit-0.4.0 → b3dkit-0.5.0}/tests/test_point.py +0 -0
  71. {b3dkit-0.4.0 → b3dkit-0.5.0}/tests/test_public_api.py +0 -0
@@ -0,0 +1,20 @@
1
+ [run]
2
+ source = src/b3dkit
3
+ # `*test*` also matched any source path containing "test", so it was scoped to
4
+ # the test directory instead.
5
+ omit =
6
+ tests/*
7
+
8
+ [report]
9
+ show_missing = True
10
+ # CONTRIBUTING.md declares a 90% floor with no ratchet. Enforced here so the
11
+ # number is a gate rather than a suggestion.
12
+ fail_under = 90
13
+ exclude_lines =
14
+ pragma: no cover
15
+ # The __main__ blocks are demos, not library code. They were previously
16
+ # executed by ten tests that asserted nothing, purely so these lines would
17
+ # count toward coverage; deleting those tests is what this excludes for.
18
+ if __name__ == .__main__.:
19
+ # Raised only when the package is not installed, e.g. run from a source tree.
20
+ except PackageNotFoundError:
@@ -7,6 +7,41 @@ ship in minor releases.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.5.0] - 2026-09-09
11
+
12
+ ### Changed
13
+
14
+ - **`NutCut` now sits on the origin rather than straddling it.** The recess ran
15
+ from `-head_depth` to `0` while the shaft began at `+head_depth`, so the two
16
+ never met: the cutter came back as two solids with a `head_depth` gap, and
17
+ subtracting it left a membrane the bolt could not pass through. It is now one
18
+ solid spanning `0` to `head_depth + shaft_length`.
19
+
20
+ **This moves the cutter.** It previously had to be positioned *below* a
21
+ surface to work; it can now be placed *on* one. Anything calling `NutCut` and
22
+ compensating for the old placement needs that compensation removed.
23
+
24
+ - **`dovetail_subpart` no longer adds each click-fit divot twice.** One copy
25
+ landed outside the part as a free-floating sliver. Only geometry using
26
+ `click_fit_radius` is affected; the tail's fused solid is unchanged and the
27
+ socket's moves by 0.02%.
28
+
29
+ - **A positive `vertical_offset` no longer cuts the dovetail socket in two.**
30
+ Tails, `vertical_offset=0` and negative offsets are all unchanged.
31
+
32
+ ### Known issues
33
+
34
+ Found while adding solid-count assertions, all pre-existing and all invisible to
35
+ the `is_valid` checks that used to stand in for them:
36
+
37
+ - `TwistSnapConnector` returns five solids: a base and four snapfits that meet
38
+ only on a shared plane and never fuse.
39
+ - `HexWall` can return partial hexagons clipped at the bounds as separate
40
+ slivers, depending on how the grid divides the given width.
41
+ - `SNUGTAIL` splits its own tail into disconnected fins on parts wider than
42
+ about 30mm. Pinned by `TestSnugtailWidthLimit`.
43
+
44
+
10
45
  ## [0.4.0] - 2026-09-08
11
46
 
12
47
  ### Removed
@@ -111,6 +146,7 @@ the Python 3.11 floor.
111
146
  See the git history. b3dkit began as `fb-library`; the rename accompanied a
112
147
  rewrite that reworked names and usage to feel closer to native build123d.
113
148
 
114
- [Unreleased]: https://github.com/x0pherl/b3dkit/compare/v0.4.0...HEAD
149
+ [Unreleased]: https://github.com/x0pherl/b3dkit/compare/v0.5.0...HEAD
150
+ [0.5.0]: https://github.com/x0pherl/b3dkit/releases/tag/v0.5.0
115
151
  [0.4.0]: https://github.com/x0pherl/b3dkit/releases/tag/v0.4.0
116
152
  [0.3.2]: https://github.com/x0pherl/b3dkit/releases/tag/v0.3.2
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: b3dkit
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: build123d libraries and utilities
5
5
  Project-URL: Homepage, https://github.com/x0pherl/b3dkit
6
6
  Project-URL: Issues, https://github.com/x0pherl/b3dkit/issues
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "b3dkit"
3
- version = "0.4.0"
3
+ version = "0.5.0"
4
4
  authors = [
5
5
  { name="x0pherl"},
6
6
  ]
@@ -63,8 +63,15 @@ build-backend = "hatchling.build"
63
63
  [tool.pytest.ini_options]
64
64
  pythonpath = ["src"]
65
65
  addopts = "--strict-markers"
66
- markers = [
67
- "manual: marks tests that can only be executed manually",
66
+ filterwarnings = [
67
+ # b3dkit's own warnings are failures; we control those.
68
+ "error",
69
+ # Everything below is raised inside the dependency stack, not by b3dkit.
70
+ # ezdxf, reached through build123d, calls pyparsing's pre-snake_case API.
71
+ "ignore::DeprecationWarning:ezdxf.*",
72
+ "ignore::DeprecationWarning:pyparsing.*",
73
+ "default::DeprecationWarning:build123d.*",
74
+ "default::PendingDeprecationWarning:build123d.*",
68
75
  ]
69
76
 
70
77
  [tool.black]
@@ -251,13 +251,13 @@ class NutCut(BasePartObject):
251
251
  context: BuildPart = BuildPart._get_context()
252
252
  validate_inputs(context, self)
253
253
 
254
- with BuildPart(Location((0, 0, head_depth))) as cut:
254
+ with BuildPart() as cut:
255
255
  with BuildSketch():
256
256
  RegularPolygon(radius=head_radius, side_count=6)
257
- extrude(amount=-head_depth)
257
+ extrude(amount=head_depth)
258
258
  Cylinder(
259
259
  radius=shaft_radius,
260
- height=shaft_length,
260
+ height=shaft_length + head_depth,
261
261
  align=(Align.CENTER, Align.CENTER, Align.MIN),
262
262
  )
263
263
 
@@ -528,18 +528,10 @@ def _traditional_subpart_divots(
528
528
  ),
529
529
  mode=topmode,
530
530
  ):
531
- add(
532
- Divot(
533
- click_fit_radius,
534
- positive=topmode == Mode.ADD,
535
- extend_base=True,
536
- )
537
- .rotate(
538
- Axis.X,
539
- (90 * (-1 if vertical_offset < 0 else 1))
540
- + adjusted_top_divot_angle,
541
- )
542
- .rotate(Axis.Y, cut_angle),
531
+ Divot(
532
+ click_fit_radius,
533
+ positive=topmode == Mode.ADD,
534
+ extend_base=True,
543
535
  )
544
536
  #####################################
545
537
  # Bottom divots
@@ -562,29 +554,20 @@ def _traditional_subpart_divots(
562
554
  ),
563
555
  mode=bottommode,
564
556
  ):
565
- add(
566
- Divot(
567
- click_fit_radius,
568
- positive=bottommode == Mode.ADD,
569
- extend_base=True,
570
- )
571
- .rotate(
572
- Axis.X,
573
- (90 * (1 if vertical_offset < 0 else -1)) + scarf_angle,
574
- )
575
- .rotate(Axis.Y, cut_angle),
557
+ Divot(
558
+ click_fit_radius,
559
+ positive=bottommode == Mode.ADD,
560
+ extend_base=True,
576
561
  )
577
562
  with BuildPart(
578
563
  Location((end_side.x, end_side.y, click_fit_radius * 2)),
579
564
  mode=bottommode,
580
565
  ):
581
- add(
582
- Divot(click_fit_radius, positive=True, extend_base=True)
583
- .rotate(
584
- Axis.X,
585
- (90 * (1 if vertical_offset < 0 else -1)) + scarf_angle,
586
- )
587
- .rotate(Axis.Y, cut_angle),
566
+ Divot(
567
+ click_fit_radius,
568
+ # was hardcoded True while the sibling divot above derives it
569
+ positive=bottommode == Mode.ADD,
570
+ extend_base=True,
588
571
  )
589
572
 
590
573
  return divotedpart.part
@@ -954,7 +937,16 @@ def dovetail_subpart(
954
937
  current_floor = (
955
938
  part.bounding_box().min.Z
956
939
  if vertical_offset <= 0
957
- else part.bounding_box().min.Z + abs(vertical_offset) + vertical_tolerance
940
+ # Must match the straightened slab's top_z above, which uses
941
+ # vertical_tolerance_adjustment. Using the raw vertical_tolerance
942
+ # here agreed for the tail, where the adjustment is +tolerance, and
943
+ # disagreed for the socket, where it is -tolerance: the two slabs
944
+ # then missed each other by 2 * vertical_tolerance, cutting a slot
945
+ # clean through the socket and leaving it in two pieces. The
946
+ # vertical_offset < 0 branch below already uses the adjustment.
947
+ else part.bounding_box().min.Z
948
+ + abs(vertical_offset)
949
+ + vertical_tolerance_adjustment
958
950
  )
959
951
  add(
960
952
  _subpart_slab(
@@ -1,7 +1,3 @@
1
- from importlib.machinery import SourceFileLoader
2
- from importlib.util import module_from_spec, spec_from_loader
3
- from unittest.mock import patch
4
-
5
1
  import pytest
6
2
  from build123d import (
7
3
  Align,
@@ -16,7 +12,6 @@ from build123d import (
16
12
  )
17
13
 
18
14
  from b3dkit.antichamfer import anti_chamfer
19
- from conftest import module_path
20
15
 
21
16
 
22
17
  class TestAntiChamfer:
@@ -122,7 +117,10 @@ class TestAntiChamfer:
122
117
  Box(10, 10, 10)
123
118
 
124
119
  ac_part = anti_chamfer(bp.part.faces().filter_by(Axis.Z)[-1], -1.0, -0.5)
125
- assert ac_part.is_valid
120
+ assert len(ac_part.solids()) == 1
121
+ # negative lengths still extend the face outward rather than cutting in
122
+ assert ac_part.volume == pytest.approx(1110.3333, rel=1e-4)
123
+ assert ac_part.volume > bp.part.volume
126
124
 
127
125
  def test_anti_chamfer_zero_length_values(self):
128
126
  """Test anti_chamfer with negative length values"""
@@ -136,19 +134,6 @@ class TestAntiChamfer:
136
134
  assert ac2.is_valid
137
135
  assert bp.part.volume == ac1.volume == ac2.volume
138
136
 
139
- def test_direct_run(self):
140
-
141
- with (
142
- patch("build123d.export_stl"),
143
- patch("pathlib.Path.mkdir"),
144
- patch("pathlib.Path.exists"),
145
- patch("pathlib.Path.is_dir"),
146
- patch("ocp_vscode.show"),
147
- patch("ocp_vscode.save_screenshot"),
148
- ):
149
- loader = SourceFileLoader("__main__", module_path("antichamfer"))
150
- loader.exec_module(module_from_spec(spec_from_loader(loader.name, loader)))
151
-
152
137
 
153
138
  class TestAntiChamferBuilderContract:
154
139
  """anti_chamfer applies to BuildPart only, and must say so.
@@ -1,13 +1,9 @@
1
1
  import math
2
- from importlib.machinery import SourceFileLoader
3
- from importlib.util import module_from_spec, spec_from_loader
4
- from unittest.mock import patch
5
2
 
6
3
  import pytest
7
4
  from build123d import Part
8
5
 
9
6
  from b3dkit.ball_socket import BallMount, BallSocket
10
- from conftest import module_path
11
7
 
12
8
 
13
9
  # ---------- Helpers ----------
@@ -45,8 +41,11 @@ class TestBallSocket:
45
41
  @pytest.mark.parametrize("r,w", [(3, 2), (5, 1), (10, 2), (20, 4), (12.5, 3.5)])
46
42
  def test_ball_socket_param_dimensions(self, r, w):
47
43
  socket = BallSocket(r, wall_thickness=w)
48
- assert socket.is_valid
44
+ assert len(socket.solids()) == 1
49
45
  bbox = socket.bounding_box()
46
+ # the socket encloses the ball plus a wall on each side
47
+ assert bbox.size.X == pytest.approx(2 * r + 2 * w)
48
+ assert socket.volume > 0
50
49
 
51
50
  def test_ball_socket_custom_wall_thickness(self):
52
51
  r, w = 10.0, 3.0
@@ -97,11 +96,3 @@ class TestEdgeCases:
97
96
  assert tight.is_valid
98
97
  assert loose.is_valid
99
98
  assert loose.volume < BallSocket(10.0, tolerance=0.0).volume
100
-
101
-
102
- # ---------- Direct Run ----------
103
- class TestDirectRun:
104
- def test_direct_run(self):
105
- with patch("ocp_vscode.show"):
106
- loader = SourceFileLoader("__main__", module_path("ball_socket"))
107
- loader.exec_module(module_from_spec(spec_from_loader(loader.name, loader)))
@@ -1,7 +1,3 @@
1
- from importlib.machinery import SourceFileLoader
2
- from importlib.util import module_from_spec, spec_from_loader
3
- from unittest.mock import patch
4
-
5
1
  import pytest
6
2
  from build123d import Align, Part
7
3
 
@@ -19,7 +15,6 @@ from b3dkit.basic_shapes import (
19
15
  opposite_length,
20
16
  radius_to_apothem,
21
17
  )
22
- from conftest import module_path
23
18
 
24
19
 
25
20
  class TestApothemConversions:
@@ -265,13 +260,3 @@ class TestDiamondCylinder:
265
260
  assert db.Y == pytest.approx(pb.Y)
266
261
  assert db.Z == pytest.approx(pb.Z)
267
262
  assert diamond.volume == pytest.approx(poly4.volume)
268
-
269
-
270
- class TestBareExecution:
271
- def test_bare_execution(self):
272
- with (
273
- patch("pathlib.Path.mkdir"),
274
- patch("ocp_vscode.show"),
275
- ):
276
- loader = SourceFileLoader("__main__", module_path("basic_shapes"))
277
- loader.exec_module(module_from_spec(spec_from_loader(loader.name, loader)))
@@ -9,10 +9,6 @@ These tests cover all functions and their parameters to ensure:
9
9
  - Mutation testing coverage
10
10
  """
11
11
 
12
- from importlib.machinery import SourceFileLoader
13
- from importlib.util import module_from_spec, spec_from_loader
14
- from unittest.mock import patch
15
-
16
12
  import pytest
17
13
  from build123d import (
18
14
  Part,
@@ -26,7 +22,6 @@ from b3dkit.bolt_fittings import (
26
22
  SquareNutSinkhole,
27
23
  TeardropBoltCutSinkhole,
28
24
  )
29
- from conftest import module_path
30
25
 
31
26
 
32
27
  class TestTeardropBoltCutSinkhole:
@@ -34,19 +29,27 @@ class TestTeardropBoltCutSinkhole:
34
29
  """Test teardrop bolt cut with default parameters"""
35
30
  sinkhole = TeardropBoltCutSinkhole()
36
31
 
37
- assert sinkhole.is_valid
32
+ assert len(sinkhole.solids()) == 1
33
+ assert sinkhole.volume == pytest.approx(3226.9861, rel=1e-4)
34
+ assert sinkhole.bounding_box().size.Z == pytest.approx(107.0)
38
35
 
39
36
  def test_teardrop_bolt_cut_custom_shaft(self):
40
37
  """Test teardrop bolt cut with custom shaft dimensions"""
41
38
  sinkhole = TeardropBoltCutSinkhole(shaft_radius=2.0, shaft_depth=5.0)
42
39
 
43
- assert sinkhole.is_valid
40
+ assert len(sinkhole.solids()) == 1
41
+ assert sinkhole.volume == pytest.approx(3273.1276, rel=1e-4)
42
+ # a deeper shaft makes the cutter taller
43
+ assert sinkhole.bounding_box().size.Z == pytest.approx(110.0)
44
44
 
45
45
  def test_teardrop_bolt_cut_custom_head(self):
46
46
  """Test teardrop bolt cut with custom head dimensions"""
47
47
  sinkhole = TeardropBoltCutSinkhole(head_radius=4.0, head_depth=3.0)
48
48
 
49
- assert sinkhole.is_valid
49
+ assert len(sinkhole.solids()) == 1
50
+ assert sinkhole.volume == pytest.approx(5255.4069, rel=1e-4)
51
+ # a wider head widens the cutter beyond the default 8.2mm
52
+ assert sinkhole.bounding_box().size.X == pytest.approx(10.0)
50
53
 
51
54
  def test_teardrop_bolt_cut_with_chamfer(self):
52
55
  """Test teardrop bolt cut with various chamfer radii"""
@@ -89,7 +92,11 @@ class TestBoltCutSinkhole:
89
92
  """Test bolt cut with default parameters"""
90
93
  boltcut = BoltCutSinkhole()
91
94
 
92
- assert boltcut.is_valid
95
+ assert len(boltcut.solids()) == 1
96
+ assert boltcut.volume == pytest.approx(3197.9162, rel=1e-4)
97
+ # cylindrical, so square in plan unlike the teardrop
98
+ bbox = boltcut.bounding_box()
99
+ assert bbox.size.X == pytest.approx(bbox.size.Y)
93
100
 
94
101
  def test_bolt_cut_with_chamfer(self):
95
102
  """Test bolt cut with various chamfer radii"""
@@ -112,7 +119,10 @@ class TestBoltCutSinkhole:
112
119
  """Test bolt cut with zero extension (blind hole)"""
113
120
  boltcut = BoltCutSinkhole(extension_distance=0)
114
121
 
115
- assert boltcut.is_valid
122
+ assert len(boltcut.solids()) == 1
123
+ assert boltcut.volume == pytest.approx(178.8456, rel=1e-4)
124
+ # a blind hole is only as deep as the head and shaft
125
+ assert boltcut.bounding_box().size.Z == pytest.approx(7.0)
116
126
 
117
127
  def test_bolt_cut_vs_teardrop(self):
118
128
  """Test that bolt_cut and TeardropBoltCutSinkhole with default ratio produce different results"""
@@ -158,7 +168,10 @@ class TestSquareNutSinkhole:
158
168
  """Test square nut sinkhole with default parameters"""
159
169
  sinkhole = SquareNutSinkhole()
160
170
 
161
- assert sinkhole.is_valid
171
+ assert len(sinkhole.solids()) == 1
172
+ assert sinkhole.volume == pytest.approx(1267.7482, rel=1e-4)
173
+ # the nut trap sets the width: 5.6mm across flats for an M3 nut
174
+ assert sinkhole.bounding_box().size.X == pytest.approx(5.6)
162
175
 
163
176
  def test_square_nut_with_extension(self):
164
177
  """Test square nut sinkhole with bolt extension"""
@@ -171,7 +184,9 @@ class TestSquareNutSinkhole:
171
184
  """Test square nut sinkhole with zero extension"""
172
185
  sinkhole = SquareNutSinkhole(bolt_extension=0)
173
186
 
174
- assert sinkhole.is_valid
187
+ assert len(sinkhole.solids()) == 1
188
+ assert sinkhole.volume == pytest.approx(1259.1175, rel=1e-4)
189
+ assert sinkhole.volume < SquareNutSinkhole().volume
175
190
 
176
191
  def test_square_nut_different_nut_sizes(self):
177
192
  """Test with different nut sizes"""
@@ -193,8 +208,27 @@ class TestScrewCut:
193
208
  assert screw.bounding_box().size.Z == pytest.approx(20)
194
209
 
195
210
  def test_nut_cut(self):
211
+ """The cutter sits on the origin, in one piece.
212
+
213
+ It used to straddle it: the recess ran from -head_depth to 0 while the
214
+ shaft began at +head_depth, so the two never met and the cutter had to
215
+ be positioned below a surface rather than on it.
216
+ """
196
217
  nut = NutCut(5, 1, 2, 10)
197
- assert nut.is_valid
218
+ assert len(nut.solids()) == 1
219
+ bbox = nut.bounding_box()
220
+ assert bbox.min.Z == pytest.approx(0.0)
221
+ assert bbox.max.Z == pytest.approx(11.0)
222
+
223
+ @pytest.mark.parametrize("head_depth", [0.5, 1, 5])
224
+ @pytest.mark.parametrize("shaft_length", [6, 10, 20])
225
+ def test_nut_cut_dimensions(self, head_depth, shaft_length):
226
+ """The shaft extends shaft_length beyond the recess, from the origin."""
227
+ nut = NutCut(5, head_depth, 2, shaft_length)
228
+ assert len(nut.solids()) == 1
229
+ bbox = nut.bounding_box()
230
+ assert bbox.min.Z == pytest.approx(0.0)
231
+ assert bbox.max.Z == pytest.approx(head_depth + shaft_length)
198
232
 
199
233
  def test_invalid_screw_cut(self):
200
234
  with pytest.raises(ValueError):
@@ -206,10 +240,3 @@ class TestScrewCut:
206
240
  assert heatsink.bounding_box().size.X == pytest.approx(20)
207
241
  assert heatsink.bounding_box().size.Y == pytest.approx(20)
208
242
  assert heatsink.bounding_box().size.Z == pytest.approx(11)
209
-
210
-
211
- class TestBareExecution:
212
- def test_bare_execution(self):
213
- with (patch("ocp_vscode.show"),):
214
- loader = SourceFileLoader("__main__", module_path("bolt_fittings"))
215
- loader.exec_module(module_from_spec(spec_from_loader(loader.name, loader)))
@@ -0,0 +1,19 @@
1
+ import pytest
2
+
3
+ from b3dkit.click_fit import Divot
4
+
5
+
6
+ class TestClickfit:
7
+ def test_divot(self):
8
+ hole = Divot(10, False)
9
+ assert hole.is_valid
10
+ bump = Divot(10, True)
11
+ assert bump.is_valid
12
+ assert hole.volume > bump.volume
13
+
14
+ def test_divot_extend_base(self):
15
+ bump = Divot(10, True, extend_base=True)
16
+ assert len(bump.solids()) == 1
17
+ assert bump.volume == pytest.approx(3676.0264, rel=1e-4)
18
+ # extending the base makes it taller than the plain divot
19
+ assert bump.bounding_box().size.Z > Divot(10, True).bounding_box().size.Z
@@ -1,7 +1,3 @@
1
- from importlib.machinery import SourceFileLoader
2
- from importlib.util import module_from_spec, spec_from_loader
3
- from unittest.mock import patch
4
-
5
1
  import pytest
6
2
  from build123d import Align, Box, BuildPart, Mode, Part, add
7
3
 
@@ -14,23 +10,10 @@ from b3dkit.dovetail import (
14
10
  dovetail_subpart,
15
11
  )
16
12
  from b3dkit.point import Point
17
- from conftest import module_path
18
13
 
19
14
 
20
15
  class TestDovetail:
21
16
 
22
- def test_direct_run(self):
23
- with (
24
- patch("build123d.export_stl"),
25
- patch("pathlib.Path.mkdir"),
26
- patch("pathlib.Path.exists"),
27
- patch("pathlib.Path.is_dir"),
28
- patch("ocp_vscode.show"),
29
- patch("ocp_vscode.save_screenshot"),
30
- ):
31
- loader = SourceFileLoader("__main__", module_path("dovetail"))
32
- loader.exec_module(module_from_spec(spec_from_loader(loader.name, loader)))
33
-
34
17
  def test_start_end_match(self):
35
18
  with BuildPart(mode=Mode.PRIVATE) as test:
36
19
  Box(10, 50, 2, align=(Align.CENTER, Align.CENTER, Align.MIN))
@@ -86,7 +69,10 @@ class TestDovetail:
86
69
  click_fit_radius=0.5,
87
70
  ),
88
71
  )
89
- assert tail.part.is_valid
72
+ assert len(tail.part.solids()) == 1
73
+ assert tail.part.volume == pytest.approx(505.6255, rel=1e-4)
74
+ # the tail keeps the tongue, so it is longer in Y than the socket
75
+ assert tail.part.bounding_box().size.Y == pytest.approx(26.8917, rel=1e-4)
90
76
 
91
77
  def test_valid_socket(self):
92
78
  with BuildPart(mode=Mode.PRIVATE) as test:
@@ -103,7 +89,8 @@ class TestDovetail:
103
89
  vertical_offset=-0.5,
104
90
  ),
105
91
  )
106
- assert socket.part.is_valid
92
+ assert len(socket.part.solids()) == 1
93
+ assert socket.part.volume == pytest.approx(650.0268, rel=1e-4)
107
94
 
108
95
  def test_raises_invalid_style_for_snugtail(self):
109
96
  with pytest.raises(ValueError):
@@ -130,7 +117,8 @@ class TestDovetail:
130
117
  vertical_offset=-0.5,
131
118
  ),
132
119
  )
133
- assert socket.part.is_valid
120
+ assert len(socket.part.solids()) == 1
121
+ assert socket.part.volume == pytest.approx(509.5927, rel=1e-4)
134
122
 
135
123
  def test_valid_tslot_tail(self):
136
124
  with BuildPart(mode=Mode.PRIVATE) as test:
@@ -148,7 +136,8 @@ class TestDovetail:
148
136
  vertical_offset=-0.5,
149
137
  ),
150
138
  )
151
- assert tail.part.is_valid
139
+ assert len(tail.part.solids()) == 1
140
+ assert tail.part.volume == pytest.approx(492.0199, rel=1e-4)
152
141
 
153
142
  def test_valid_snugtail_tail(self):
154
143
  with BuildPart(mode=Mode.PRIVATE) as test:
@@ -165,7 +154,10 @@ class TestDovetail:
165
154
  click_fit_radius=1,
166
155
  ),
167
156
  )
168
- assert tail.part.is_valid
157
+ assert len(tail.part.solids()) == 1
158
+ assert tail.part.volume == pytest.approx(346.5381, rel=1e-4)
159
+ # the click-fit divot stands proud of the 2mm part
160
+ assert tail.part.bounding_box().max.Z == pytest.approx(2.5)
169
161
 
170
162
  def test_valid_snugtail_socket(self):
171
163
  with BuildPart(mode=Mode.PRIVATE) as test:
@@ -184,7 +176,10 @@ class TestDovetail:
184
176
  click_fit_radius=1,
185
177
  ),
186
178
  )
187
- assert socket.part.is_valid
179
+ assert len(socket.part.solids()) == 1
180
+ assert socket.part.volume == pytest.approx(648.4621, rel=1e-4)
181
+ # the snugtail socket wraps three sides, so it is much the larger half
182
+ assert socket.part.volume > 600
188
183
 
189
184
  def test_snugtail_ratios_exceed_max(self):
190
185
  with BuildPart(mode=Mode.PRIVATE) as test:
@@ -377,11 +372,15 @@ class TestStyleConditionalArguments:
377
372
  ],
378
373
  )
379
374
  def test_applicable_argument_accepted(self, style, param, value):
380
- assert _subpart(style=style, **{param: value}).is_valid
375
+ part = _subpart(style=style, **{param: value})
376
+ assert part.volume > 0
377
+ assert part.bounding_box().size.Z > 0
381
378
 
382
379
  @pytest.mark.parametrize("style", list(DovetailStyle))
383
380
  def test_defaults_never_raise(self, style):
384
- assert _subpart(style=style).is_valid
381
+ part = _subpart(style=style)
382
+ assert part.volume > 0
383
+ assert part.bounding_box().size.Z > 0
385
384
 
386
385
 
387
386
  class TestDovetailSplit:
@@ -449,3 +448,116 @@ class TestDefaultGeometryUnchanged:
449
448
  ):
450
449
  part = _subpart(style=style, subpart=section)
451
450
  assert part.volume == pytest.approx(expected_volume, abs=1e-4)
451
+
452
+
453
+ def _narrow_box() -> Part:
454
+ """A part narrow enough that snugtail's fins meet.
455
+
456
+ Snugtail splits its own tail into disconnected fins once the part is about
457
+ 30mm wide -- see TestSnugtailWidthLimit -- so the single-solid tests use a
458
+ width below that, to guard the defects they are about is_valid rather than
459
+ that separate one.
460
+ """
461
+ with BuildPart(mode=Mode.PRIVATE) as test:
462
+ Box(20, 50, 10, align=(Align.CENTER, Align.CENTER, Align.MIN))
463
+ return test.part
464
+
465
+
466
+ def _narrow_subpart(**kwargs) -> Part:
467
+ return dovetail_subpart(_narrow_box(), Point(-10, 0), Point(10, 0), **kwargs)
468
+
469
+
470
+ class TestSubpartsAreSingleSolids:
471
+ """A subpart must come back in one piece, whatever the arguments.
472
+
473
+ Two defects hid here for the library's whole history, both invisible to an
474
+ is_valid assertion because every piece was individually a valid solid.
475
+
476
+ A positive vertical_offset built the two Z-slabs with mismatched tolerance
477
+ terms, cutting a slot clean through the socket and leaving it in two pieces
478
+ with a tenth of its volume gone.
479
+
480
+ click_fit_radius added each divot twice: a Divot is a BasePartObject, so it
481
+ registers into the enclosing builder on construction, and the code then
482
+ added a rotated copy as well. Because rotate() turns about the global
483
+ origin, the builder's Location translated that copy again and it landed
484
+ outside the part as a free-floating sliver.
485
+ """
486
+
487
+ @pytest.mark.parametrize("style", list(DovetailStyle))
488
+ @pytest.mark.parametrize("vertical_offset", [0.0, 0.5, -0.5])
489
+ @pytest.mark.parametrize("click_fit_radius", [0.0, 0.5])
490
+ @pytest.mark.parametrize("subpart", [DovetailSubpart.TAIL, DovetailSubpart.SOCKET])
491
+ def test_subpart_is_one_solid(
492
+ self, style, vertical_offset, click_fit_radius, subpart
493
+ ):
494
+ part = _narrow_subpart(
495
+ style=style,
496
+ subpart=subpart,
497
+ vertical_offset=vertical_offset,
498
+ click_fit_radius=click_fit_radius,
499
+ )
500
+ solids = part.solids()
501
+ assert len(solids) == 1, (
502
+ f"{style.name} {subpart.name} at vertical_offset={vertical_offset}, "
503
+ f"click_fit_radius={click_fit_radius} came back as {len(solids)} "
504
+ f"solids: {sorted(round(s.volume, 4) for s in solids)}"
505
+ )
506
+
507
+ @pytest.mark.parametrize("style", list(DovetailStyle))
508
+ @pytest.mark.parametrize("vertical_offset", [0.0, 0.5, -0.5])
509
+ def test_tail_and_socket_account_for_the_whole_part(self, style, vertical_offset):
510
+ """Together the two subparts should be the original, less tolerance.
511
+
512
+ The vertical_offset defect showed up here first: the pair summed to 899
513
+ of a 1000mm3 box, because the slot cut through the socket removed
514
+ material that ended up in neither half.
515
+ """
516
+ kwargs = dict(style=style, vertical_offset=vertical_offset)
517
+ total = sum(
518
+ _narrow_subpart(subpart=s, **kwargs).volume
519
+ for s in (DovetailSubpart.TAIL, DovetailSubpart.SOCKET)
520
+ )
521
+ box_volume = 20 * 50 * 10
522
+ assert total == pytest.approx(box_volume, rel=0.02), (
523
+ f"{style.name} at vertical_offset={vertical_offset}: tail and socket "
524
+ f"sum to {total:.1f}, not the {box_volume} of the part they split"
525
+ )
526
+
527
+
528
+ class TestSnugtailWidthLimit:
529
+ """Snugtail splits its own tail into fins on parts wider than about 30mm.
530
+
531
+ Not a regression: v0.1.5 produces byte-identical geometry. The tail comes
532
+ back as two or three disconnected pieces with no sliver involved, so it is
533
+ unrelated to the divot and vertical_offset defects fixed alongside these
534
+ tests. It lives in the snugtail outline maths, which was tuned against
535
+ physical prints, so it is recorded here rather than guessed at.
536
+
537
+ These tests pin the current boundary. If a fix lands, they should fail and
538
+ be replaced by an assertion that wide parts work.
539
+ """
540
+
541
+ @staticmethod
542
+ def _tail_solids(width):
543
+ with BuildPart(mode=Mode.PRIVATE) as test:
544
+ Box(width, 60, 30, align=(Align.CENTER, Align.CENTER, Align.MIN))
545
+ return len(
546
+ dovetail_subpart(
547
+ test.part,
548
+ Point(-width / 2, 0),
549
+ Point(width / 2, 0),
550
+ subpart=DovetailSubpart.TAIL,
551
+ style=DovetailStyle.SNUGTAIL,
552
+ click_fit_radius=0,
553
+ ).solids()
554
+ )
555
+
556
+ @pytest.mark.parametrize("width", [20, 24, 28])
557
+ def test_narrow_parts_give_one_solid(self, width):
558
+ assert self._tail_solids(width) == 1
559
+
560
+ @pytest.mark.parametrize("width", [30, 36, 40])
561
+ def test_wide_parts_currently_fragment(self, width):
562
+ """Documents a known defect. Delete this when snugtail is fixed."""
563
+ assert self._tail_solids(width) > 1
@@ -1,14 +1,10 @@
1
1
  from collections import defaultdict
2
- from importlib.machinery import SourceFileLoader
3
- from importlib.util import module_from_spec, spec_from_loader
4
2
  from math import atan2, cos, degrees, sin
5
- from unittest.mock import patch
6
3
 
7
4
  import pytest
8
5
  from build123d import Align, Box, Cone, Cylinder
9
6
 
10
7
  from b3dkit.hexwall import HexCylindrical, HexWall
11
- from conftest import module_path
12
8
 
13
9
 
14
10
  def _cone():
@@ -19,26 +15,21 @@ def _cone():
19
15
  class TestHexWall:
20
16
  def test_hexwall(self):
21
17
  pattern = HexWall(10, 10, 1, 1, 0.2)
22
- assert pattern.is_valid
18
+ assert pattern.volume == pytest.approx(18.5680, rel=1e-4)
19
+ assert pattern.bounding_box().size.X == pytest.approx(10)
20
+ assert pattern.bounding_box().size.Y == pytest.approx(10)
21
+ # 3 solids: the field, plus a 0.1mm3 partial hexagon clipped at each of
22
+ # the left and right bounds. Suspect, but pinned rather than changed.
23
+ assert len(pattern.solids()) == 3
23
24
 
24
25
  def test_hexwall_forces_odd_column_count(self):
25
26
  # length=13/apothem=1 yields an even raw column count, exercising the
26
27
  # branch that bumps it odd so the hex grid stays centered
27
28
  pattern = HexWall(13, 10, 1, 1, 0.2)
28
- assert pattern.is_valid
29
-
30
- def test_direct_run(self):
31
-
32
- with (
33
- patch("build123d.export_stl"),
34
- patch("pathlib.Path.mkdir"),
35
- patch("pathlib.Path.exists"),
36
- patch("pathlib.Path.is_dir"),
37
- patch("ocp_vscode.show"),
38
- patch("ocp_vscode.save_screenshot"),
39
- ):
40
- loader = SourceFileLoader("__main__", module_path("hexwall"))
41
- loader.exec_module(module_from_spec(spec_from_loader(loader.name, loader)))
29
+ assert pattern.volume == pytest.approx(23.2053, rel=1e-4)
30
+ assert pattern.bounding_box().size.X == pytest.approx(13)
31
+ # the odd column count leaves no clipped hexagons at the bounds
32
+ assert len(pattern.solids()) == 1
42
33
 
43
34
 
44
35
  class TestHexCylindrical:
@@ -1,7 +1,3 @@
1
- from importlib.machinery import SourceFileLoader
2
- from importlib.util import module_from_spec, spec_from_loader
3
- from unittest.mock import patch
4
-
5
1
  import pytest
6
2
  from build123d import Align, Axis, Box, BuildPart, Compound, Part, fillet
7
3
 
@@ -12,7 +8,6 @@ from b3dkit.high_top_slide_box import (
12
8
  high_top_slide_box_base,
13
9
  high_top_slide_box_lid,
14
10
  )
15
- from conftest import module_path
16
11
 
17
12
 
18
13
  def assert_single_solid(part, name="part"):
@@ -284,19 +279,6 @@ class TestHighTopSlideBox:
284
279
  assert_single_solid(result.children[0], "lid")
285
280
  assert_single_solid(result.children[1], "base")
286
281
 
287
- def test_direct_run(self):
288
- """Test that the module can be run directly without errors."""
289
- with (
290
- patch("ocp_vscode.show"),
291
- (
292
- patch("build123d.export_stl")
293
- if hasattr(__import__("build123d"), "export_stl")
294
- else patch("builtins.open")
295
- ),
296
- ):
297
- loader = SourceFileLoader("__main__", module_path("high_top_slide_box"))
298
- loader.exec_module(module_from_spec(spec_from_loader(loader.name, loader)))
299
-
300
282
  def test_parameter_validation_edge_cases(self, small_base_part):
301
283
  """Test edge cases for parameter validation."""
302
284
  # Test with very thin walls
@@ -1,12 +1,7 @@
1
- from importlib.machinery import SourceFileLoader
2
- from importlib.util import module_from_spec, spec_from_loader
3
- from unittest.mock import patch
4
-
5
1
  import pytest
6
2
  from build123d import Align, Axis, Box, BuildPart, Plane, fillet, section
7
3
 
8
4
  from b3dkit.slide_box import _slider_template, slide_box
9
- from conftest import module_path
10
5
 
11
6
 
12
7
  class TestSlideBox:
@@ -18,21 +13,14 @@ class TestSlideBox:
18
13
  sb = slide_box(
19
14
  base_box.part, wall_thickness=2, thumb_radius=3.5, divot_radius=0.5
20
15
  )
21
- assert sb.children[0].is_valid
22
- assert sb.children[1].is_valid
23
-
24
- def test_direct_run(self):
25
-
26
- with (
27
- patch("build123d.export_stl"),
28
- patch("pathlib.Path.mkdir"),
29
- patch("pathlib.Path.exists"),
30
- patch("pathlib.Path.is_dir"),
31
- patch("ocp_vscode.show"),
32
- patch("ocp_vscode.save_screenshot"),
33
- ):
34
- loader = SourceFileLoader("__main__", module_path("slide_box"))
35
- loader.exec_module(module_from_spec(spec_from_loader(loader.name, loader)))
16
+ box, lid = sb.children
17
+ assert (box.label, lid.label) == ("box", "lid")
18
+ assert len(box.solids()) == 1
19
+ assert len(lid.solids()) == 1
20
+ assert box.volume == pytest.approx(4632.0405, rel=1e-4)
21
+ assert lid.volume == pytest.approx(1498.3503, rel=1e-4)
22
+ # the lid slides inside the box, so it must be narrower
23
+ assert lid.bounding_box().size.X < box.bounding_box().size.X
36
24
 
37
25
 
38
26
  class TestSliderDivots:
@@ -47,7 +35,8 @@ class TestSliderDivots:
47
35
  """
48
36
 
49
37
  @pytest.fixture(scope="class")
50
- def sketch(self):
38
+ @classmethod
39
+ def sketch(cls):
51
40
  with BuildPart() as blank:
52
41
  Box(60, 100, 20, align=(Align.CENTER, Align.CENTER, Align.MIN))
53
42
  fillet(blank.part.edges().filter_by(Axis.Z), radius=1.5)
@@ -1,29 +1,12 @@
1
- from importlib.machinery import SourceFileLoader
2
- from importlib.util import module_from_spec, spec_from_loader
3
- from unittest.mock import patch
4
-
5
1
  import pytest
6
2
 
7
3
  from b3dkit.twist_snap import (
8
4
  TwistSnapConnector,
9
5
  TwistSnapSocket,
10
6
  )
11
- from conftest import module_path
12
7
 
13
8
 
14
9
  class TestTwistSnap:
15
- def test_bare_execution(self):
16
- with (
17
- patch("build123d.export_stl"),
18
- patch("pathlib.Path.mkdir"),
19
- patch("pathlib.Path.exists"),
20
- patch("pathlib.Path.is_dir"),
21
- patch("ocp_vscode.show"),
22
- patch("ocp_vscode.save_screenshot"),
23
- ):
24
- loader = SourceFileLoader("__main__", module_path("twist_snap"))
25
- loader.exec_module(module_from_spec(spec_from_loader(loader.name, loader)))
26
-
27
10
  def test_twist_snap_connector(self):
28
11
  connector = TwistSnapConnector(
29
12
  connector_radius=4.5,
@@ -31,7 +14,12 @@ class TestTwistSnap:
31
14
  snapfit_radius_extension=2 * (2 / 3),
32
15
  wall_depth=2,
33
16
  )
34
- assert connector.is_valid
17
+ assert connector.volume == pytest.approx(275.6677, rel=1e-4)
18
+ assert connector.bounding_box().size.X == pytest.approx(11.5451, rel=1e-4)
19
+ # Known issue: the base and its four snapfits meet only on a shared
20
+ # plane, so they never fuse and this returns five solids. Pinned rather
21
+ # than asserted as correct; when it is fixed this should read == 1.
22
+ assert len(connector.solids()) == 5
35
23
 
36
24
  def test_connector_takes_no_clearance_arguments(self):
37
25
  """All fit clearance lives on the socket; the connector is nominal.
@@ -80,4 +68,7 @@ class TestTwistSnap:
80
68
  wall_width=2,
81
69
  wall_depth=2,
82
70
  )
83
- assert socket.is_valid
71
+ assert len(socket.solids()) == 1
72
+ assert socket.volume == pytest.approx(630.1939, rel=1e-4)
73
+ # the socket has to swallow the connector, so it is the wider part
74
+ assert socket.bounding_box().size.X > 11.5451
b3dkit-0.4.0/.coveragerc DELETED
@@ -1,6 +0,0 @@
1
- [run]
2
- omit =
3
- *test*
4
- *conftest*
5
- [report]
6
- show_missing = True
@@ -1,12 +0,0 @@
1
- import os
2
-
3
- # Absolute path to the package source, so tests that load a module by file path
4
- # work regardless of the directory pytest was invoked from.
5
- PACKAGE_DIR = os.path.abspath(
6
- os.path.join(os.path.dirname(__file__), "..", "src", "b3dkit")
7
- )
8
-
9
-
10
- def module_path(module_name: str) -> str:
11
- """Absolute path to a b3dkit source file, e.g. module_path("dovetail")."""
12
- return os.path.join(PACKAGE_DIR, f"{module_name}.py")
@@ -1,32 +0,0 @@
1
- from importlib.machinery import SourceFileLoader
2
- from importlib.util import module_from_spec, spec_from_loader
3
- from unittest.mock import patch
4
-
5
- from b3dkit.click_fit import Divot
6
- from conftest import module_path
7
-
8
-
9
- class TestClickfit:
10
- def test_divot(self):
11
- hole = Divot(10, False)
12
- assert hole.is_valid
13
- bump = Divot(10, True)
14
- assert bump.is_valid
15
- assert hole.volume > bump.volume
16
-
17
- def test_divot_extend_base(self):
18
- bump = Divot(10, True, extend_base=True)
19
- assert bump.is_valid
20
-
21
- def test_direct_run(self):
22
-
23
- with (
24
- patch("build123d.export_stl"),
25
- patch("pathlib.Path.mkdir"),
26
- patch("pathlib.Path.exists"),
27
- patch("pathlib.Path.is_dir"),
28
- patch("ocp_vscode.show"),
29
- patch("ocp_vscode.save_screenshot"),
30
- ):
31
- loader = SourceFileLoader("__main__", module_path("click_fit"))
32
- loader.exec_module(module_from_spec(spec_from_loader(loader.name, loader)))
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
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