Repository: cwacek/python-jsonschema-objects

Issue:
Unprecise comparizon with zero of float values
Consider the following schema (this is an extract from Walmart)
```
              "ShippingWeight": {
                "type": "number",
                "title": "Shipping Weight (lbs)",
                "minimum": 0,
                "maximum": 1000000000,
                "exclusiveMaximum": false,
                "multipleOf": 0.001
              },
```
While evaluating the `multipleOf` constraint the result will never be exact 0 therefore the validation will always fail.

https://github.com/cwacek/python-jsonschema-objects/blob/2a5aeb29a953cff3d4fd82f19c0d46342af1da36/python_jsonschema_objects/validators.py#L44C8-L44C11

Comments:

Patch:
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 263f065..2f214e7 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -14,7 +14,7 @@ jobs:
   deploy:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
       - name: Set up Python
         uses: actions/setup-python@v4
         with:
diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 2aa3866..b4d4943 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -15,7 +15,7 @@ jobs:
   lint:
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
     - name: Set up Python
       uses: actions/setup-python@v4
       with:
@@ -38,18 +38,19 @@ jobs:
       fail-fast: true
       matrix:
         os: [ubuntu, macos, windows]
-        python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
+        python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
         include:
           - experimental: false
-          - python-version: "3.11"
+          - python-version: "3.12"
             experimental: true
 
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
     - name: Set up Python ${{ matrix.python-version }}
       uses: actions/setup-python@v4
       with:
         python-version: ${{ matrix.python-version }}
+        allow-prereleases: ${{ matrix.experimental }}
     - name: Install dependencies
       run: |
         python -m pip install --upgrade pip
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 1624dfc..964c36b 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
 repos:
-  - repo: https://github.com/psf/black
-    rev:  23.7.0
+  - repo: https://github.com/psf/black-pre-commit-mirror
+    rev: 23.7.0
     hooks:
     - id: black
-      language_version: python3.9
+      language_version: python3.11
diff --git a/python_jsonschema_objects/validators.py b/python_jsonschema_objects/validators.py
index 5cd9c94..92a792d 100644
--- a/python_jsonschema_objects/validators.py
+++ b/python_jsonschema_objects/validators.py
@@ -1,3 +1,4 @@
+import decimal
 import logging
 
 import six
@@ -40,8 +41,14 @@ registry = ValidatorRegistry()
 
 @registry.register()
 def multipleOf(param, value, _):
-    quot, rem = divmod(value, param)
-    if rem != 0:
+    # This conversion to string is intentional because floats are imprecise.
+    # >>> decimal.Decimal(33.069)
+    # Decimal('33.0690000000000026147972675971686840057373046875')
+    # >>> decimal.Decimal('33.069')
+    # Decimal('33.069')
+    value = decimal.Decimal(str(value))
+    divisor = decimal.Decimal(str(param))
+    if value % divisor != 0:
         raise ValidationError("{0} is not a multiple of {1}".format(value, param))
 
 
diff --git a/tox.ini b/tox.ini
index dbf39b5..f8bb557 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = py{37,38,39,310,311}-jsonschema{23,24,25,26,30,40}-markdown{2,3}
+envlist = py{37,38,39,310,311,312}-jsonschema{23,24,25,26,30,40}-markdown{2,3}
 skip_missing_interpreters = true
 
 [gh-actions]
@@ -9,6 +9,7 @@ python =
   3.9: py39
   3.10: py310
   3.11: py311
+  3.12: py312
 
 [testenv]
 commands =


Test patch:
diff --git a/conftest.py b/conftest.py
index 28287ea..a8ea1c6 100644
--- a/conftest.py
+++ b/conftest.py
@@ -1,20 +1,19 @@
+import importlib.resources
+import json
+
 import pytest
 
-import json
-import pkg_resources
-import os
 import python_jsonschema_objects as pjs
-import python_jsonschema_objects.markdown_support
 
 
 @pytest.fixture
 def markdown_examples():
-    md = pkg_resources.resource_filename(
+    with importlib.resources.path(
         "python_jsonschema_objects.examples", "README.md"
-    )
-    examples = python_jsonschema_objects.markdown_support.extract_code_blocks(md)
-    examples = {json.loads(v)["title"]: json.loads(v) for v in examples["schema"]}
-    return examples
+    ) as md:
+        examples = pjs.markdown_support.extract_code_blocks(md)
+
+    return {json.loads(v)["title"]: json.loads(v) for v in examples["schema"]}
 
 
 @pytest.fixture(autouse=True)
diff --git a/test/test_pytest.py b/test/test_pytest.py
index 6fc0775..9fe2950 100644
--- a/test/test_pytest.py
+++ b/test/test_pytest.py
@@ -41,7 +41,7 @@ def test_schema_validation():
         "$id": "test",
         "type": "object",
         "properties": {
-            "name": "string",  #  <-- this is invalid
+            "name": "string",  # <-- this is invalid
             "email": {"oneOf": [{"type": "string"}, {"type": "integer"}]},
         },
         "required": ["email"],
@@ -531,3 +531,24 @@ def test_justareference_example(markdown_examples):
     )
     ns = builder.build_classes()
     ns.JustAReference("Hello")
+
+
+def test_number_multiple_of_validation():
+    schema = {
+        "$schema": "http://json-schema.org/schema#",
+        "$id": "test",
+        "type": "object",
+        "title": "Base",
+        "properties": {
+            "sample": {
+                "type": "number",
+                "minimum": 0,
+                "maximum": 1000000000,
+                "multipleOf": 0.001,
+            },
+        },
+    }
+
+    builder = pjs.ObjectBuilder(schema)
+    ns = builder.build_classes()
+    ns.Base(sample=33.069)