numclassify 0.3.3__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 (42) hide show
  1. {numclassify-0.3.3 → numclassify-0.4.0}/CHANGELOG.md +19 -0
  2. {numclassify-0.3.3 → numclassify-0.4.0}/PKG-INFO +2 -2
  3. numclassify-0.4.0/SECURITY.md +24 -0
  4. {numclassify-0.3.3 → numclassify-0.4.0}/numclassify/__init__.py +7 -1
  5. {numclassify-0.3.3 → numclassify-0.4.0}/numclassify/_core/divisors.py +2 -0
  6. {numclassify-0.3.3 → numclassify-0.4.0}/numclassify/_core/exam_types.py +4 -0
  7. {numclassify-0.3.3 → numclassify-0.4.0}/numclassify/_core/powers.py +3 -1
  8. {numclassify-0.3.3 → numclassify-0.4.0}/pyproject.toml +2 -2
  9. {numclassify-0.3.3 → numclassify-0.4.0}/tests/test_registry.py +69 -13
  10. {numclassify-0.3.3 → numclassify-0.4.0}/.github/workflows/ci.yml +0 -0
  11. {numclassify-0.3.3 → numclassify-0.4.0}/.github/workflows/docs.yml +0 -0
  12. {numclassify-0.3.3 → numclassify-0.4.0}/.github/workflows/publish.yml +0 -0
  13. {numclassify-0.3.3 → numclassify-0.4.0}/.gitignore +0 -0
  14. {numclassify-0.3.3 → numclassify-0.4.0}/CONTRIBUTING.md +0 -0
  15. {numclassify-0.3.3 → numclassify-0.4.0}/LICENSE +0 -0
  16. {numclassify-0.3.3 → numclassify-0.4.0}/README.md +0 -0
  17. {numclassify-0.3.3 → numclassify-0.4.0}/docs/api.md +0 -0
  18. {numclassify-0.3.3 → numclassify-0.4.0}/docs/changelog.md +0 -0
  19. {numclassify-0.3.3 → numclassify-0.4.0}/docs/cli.md +0 -0
  20. {numclassify-0.3.3 → numclassify-0.4.0}/docs/extra/saffron.css +0 -0
  21. {numclassify-0.3.3 → numclassify-0.4.0}/docs/index.md +0 -0
  22. {numclassify-0.3.3 → numclassify-0.4.0}/docs/playground.css +0 -0
  23. {numclassify-0.3.3 → numclassify-0.4.0}/docs/playground.html +0 -0
  24. {numclassify-0.3.3 → numclassify-0.4.0}/docs/playground.js +0 -0
  25. {numclassify-0.3.3 → numclassify-0.4.0}/examples/basic_usage.py +0 -0
  26. {numclassify-0.3.3 → numclassify-0.4.0}/examples/custom_type.py +0 -0
  27. {numclassify-0.3.3 → numclassify-0.4.0}/examples/find_perfect_numbers.py +0 -0
  28. {numclassify-0.3.3 → numclassify-0.4.0}/examples/random_classify.py +0 -0
  29. {numclassify-0.3.3 → numclassify-0.4.0}/examples/stream_large_range.py +0 -0
  30. {numclassify-0.3.3 → numclassify-0.4.0}/mkdocs.yml +0 -0
  31. {numclassify-0.3.3 → numclassify-0.4.0}/numclassify/__main__.py +0 -0
  32. {numclassify-0.3.3 → numclassify-0.4.0}/numclassify/_core/__init__.py +0 -0
  33. {numclassify-0.3.3 → numclassify-0.4.0}/numclassify/_core/combinatorial.py +0 -0
  34. {numclassify-0.3.3 → numclassify-0.4.0}/numclassify/_core/digital.py +0 -0
  35. {numclassify-0.3.3 → numclassify-0.4.0}/numclassify/_core/figurate.py +0 -0
  36. {numclassify-0.3.3 → numclassify-0.4.0}/numclassify/_core/number_theory.py +0 -0
  37. {numclassify-0.3.3 → numclassify-0.4.0}/numclassify/_core/primes.py +0 -0
  38. {numclassify-0.3.3 → numclassify-0.4.0}/numclassify/_core/recreational.py +0 -0
  39. {numclassify-0.3.3 → numclassify-0.4.0}/numclassify/_core/sequences.py +0 -0
  40. {numclassify-0.3.3 → numclassify-0.4.0}/numclassify/_registry.py +0 -0
  41. {numclassify-0.3.3 → numclassify-0.4.0}/numclassify/cli.py +0 -0
  42. {numclassify-0.3.3 → numclassify-0.4.0}/numclassify/py.typed +0 -0
@@ -6,6 +6,25 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
6
6
 
7
7
  ---
8
8
 
9
+ ## [0.4.0] - 2026-06-13
10
+
11
+ ### Fixed
12
+ - `classify()` now returns `notable_score` field — score excluding figurate/centered-figurate
13
+ hits. Prevents misleading score inflation for n=1 (which is the first member of every
14
+ polygonal sequence).
15
+ - `is_unique(n)` now returns `False` for negative `n`. Previously returned `True` due to
16
+ `abs()` stripping the sign.
17
+ - `is_practical(0)` now returns `False`. Previously returned `True` because
18
+ `_factorization(0)` returns `[]` and the loop never ran.
19
+ - Removed leaked internal names (`Optional`, `_version`, `_PackageNotFoundError`) from
20
+ `dir(numclassify)`.
21
+
22
+ ### Changed
23
+ - Development status updated to `5 - Production/Stable` in package classifiers.
24
+
25
+ ### Added
26
+ - `SECURITY.md` with vulnerability reporting instructions.
27
+
9
28
  ## [0.3.3] - 2026-06-13
10
29
 
11
30
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: numclassify
3
- Version: 0.3.3
3
+ Version: 0.4.0
4
4
  Summary: The most comprehensive Python library for number classification - 3000+ number types
5
5
  Project-URL: Homepage, https://github.com/aratrikghosh2011-tech/numclassify
6
6
  Project-URL: Repository, https://github.com/aratrikghosh2011-tech/numclassify
@@ -10,7 +10,7 @@ Author-email: Aratrik Ghosh <aratrikghosh2011@gmail.com>
10
10
  License: MIT
11
11
  License-File: LICENSE
12
12
  Keywords: armstrong,classification,figurate,mathematics,number-theory,prime
13
- Classifier: Development Status :: 4 - Beta
13
+ Classifier: Development Status :: 5 - Production/Stable
14
14
  Classifier: Intended Audience :: Education
15
15
  Classifier: Intended Audience :: Science/Research
16
16
  Classifier: License :: OSI Approved :: MIT License
@@ -0,0 +1,24 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ Only the latest release on PyPI is supported.
6
+
7
+ | Version | Supported |
8
+ |---------|-----------|
9
+ | 0.4.x | ✅ |
10
+ | < 0.4 | ❌ |
11
+
12
+ ## Reporting a Vulnerability
13
+
14
+ To report a security vulnerability, email **aratrikghosh2011@gmail.com** with
15
+ the subject line `[numclassify] Security Report`.
16
+
17
+ Please include:
18
+ - A description of the vulnerability
19
+ - Steps to reproduce
20
+ - Potential impact
21
+
22
+ You can expect an acknowledgment within 48 hours. Since `numclassify` is a
23
+ pure computation library with no network access, no file system writes, and no
24
+ external dependencies, the attack surface is minimal.
@@ -115,6 +115,10 @@ def classify(n: int) -> dict:
115
115
  "number": n,
116
116
  "true_properties": true_props,
117
117
  "score": len(true_props),
118
+ "notable_score": sum(
119
+ len(v) for k, v in categories.items()
120
+ if k not in ("figurate", "figurate_centered")
121
+ ),
118
122
  "categories": categories,
119
123
  }
120
124
 
@@ -216,6 +220,7 @@ def stream(start: int, end: int, min_score: int = 0, has_property: Optional[str]
216
220
 
217
221
  __all__ = [
218
222
  "__version__",
223
+ "register",
219
224
  "is_prime",
220
225
  "is_armstrong",
221
226
  "is_perfect",
@@ -237,4 +242,5 @@ __all__ = [
237
242
  # Clean up internal names that leak into dir(nc)
238
243
  del (_primes, _figurate, _digital, _recreational,
239
244
  _divisors, _sequences, _powers, _number_theory,
240
- _combinatorial, _exam_types, _core, _registry)
245
+ _combinatorial, _exam_types, _core, _registry,
246
+ Optional, _version, _PackageNotFoundError)
@@ -383,6 +383,8 @@ def is_practical(n: int) -> bool:
383
383
  -------
384
384
  bool
385
385
  """
386
+ if n <= 0:
387
+ return False
386
388
  if n == 1:
387
389
  return True
388
390
  if n % 2 != 0 and n != 1:
@@ -375,10 +375,14 @@ def is_unique(n: int) -> bool:
375
375
  True
376
376
  >>> is_unique(0)
377
377
  True
378
+ >>> is_unique(-5) # negative numbers are not unique
379
+ False
378
380
 
379
381
  Notes
380
382
  -----
381
383
  Single-digit numbers and 0 are always unique.
382
384
  """
385
+ if n < 0:
386
+ return False
383
387
  s = str(abs(n))
384
388
  return len(s) == len(set(s))
@@ -225,7 +225,9 @@ def is_sum_of_three_squares(n: int) -> bool:
225
225
  """
226
226
  if n < 0:
227
227
  return False
228
- # Remove factors of 4
228
+ if n == 0:
229
+ return True # 0 = 0^2 + 0^2 + 0^2
230
+ # Remove factors of 4 (Legendre's three-square theorem)
229
231
  while n % 4 == 0:
230
232
  n //= 4
231
233
  return n % 8 != 7
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "numclassify"
7
- version = "0.3.3"
7
+ version = "0.4.0"
8
8
  description = "The most comprehensive Python library for number classification - 3000+ number types"
9
9
  authors = [{name = "Aratrik Ghosh", email = "aratrikghosh2011@gmail.com"}]
10
10
  readme = "README.md"
@@ -12,7 +12,7 @@ requires-python = ">=3.8"
12
12
  license = {text = "MIT"}
13
13
  keywords = ["number-theory", "mathematics", "classification", "armstrong", "prime", "figurate"]
14
14
  classifiers = [
15
- "Development Status :: 4 - Beta",
15
+ "Development Status :: 5 - Production/Stable",
16
16
  "Intended Audience :: Education",
17
17
  "Intended Audience :: Science/Research",
18
18
  "Topic :: Scientific/Engineering :: Mathematics",
@@ -353,26 +353,82 @@ def test_is_factorial():
353
353
  # ---------------------------------------------------------------------------
354
354
 
355
355
  def _get_all_registered_funcs():
356
- """Return list of (name, func) for every canonical registry entry."""
356
+ """Return list of (name, func) for canonical registry entries.
357
+
358
+ Samples at most 10 entries from figurate / figurate_centered (since all
359
+ use the same parametric functions) and all entries from other categories.
360
+ """
357
361
  from numclassify._registry import REGISTRY, _normalize
358
362
  seen = set()
359
363
  result = []
364
+ sample = {"figurate", "figurate_centered"}
365
+ sample_count = {cat: 0 for cat in sample}
366
+ max_per_cat = 10
360
367
  for key, entry in REGISTRY.items():
361
- if key == _normalize(entry.name): # canonical key only
368
+ if key == _normalize(entry.name):
362
369
  if key not in seen:
363
370
  seen.add(key)
364
- result.append((entry.name, entry.func))
371
+ cat = entry.category
372
+ if cat in sample:
373
+ if sample_count[cat] < max_per_cat:
374
+ sample_count[cat] += 1
375
+ result.append((entry.name, entry.func))
376
+ else:
377
+ result.append((entry.name, entry.func))
365
378
  return result
366
379
 
367
380
 
368
381
  @pytest.mark.parametrize("name,func", _get_all_registered_funcs())
369
- @pytest.mark.parametrize("n", [0, 1, 2, -1])
370
- def test_no_crash_on_edge_inputs(name, func, n):
371
- """Every registered type must not raise on inputs 0, 1, 2, -1."""
372
- try:
373
- result = func(n)
374
- assert isinstance(result, bool), (
375
- f"{name}({n}) returned {type(result)}, expected bool"
376
- )
377
- except Exception as e:
378
- pytest.fail(f"{name}({n}) raised {type(e).__name__}: {e}")
382
+ def test_no_crash_on_edge_inputs(name, func):
383
+ """Every registered type must not raise on inputs 0, 1, 2."""
384
+ for n in [0, 1, 2]:
385
+ try:
386
+ result = func(n)
387
+ assert isinstance(result, bool), (
388
+ f"{name}({n}) returned {type(result)}, expected bool"
389
+ )
390
+ except Exception as e:
391
+ pytest.fail(f"{name}({n}) raised {type(e).__name__}: {e}")
392
+
393
+
394
+ # ---------------------------------------------------------------------------
395
+ # v0.4.0 regression tests
396
+ # ---------------------------------------------------------------------------
397
+
398
+ def test_classify_has_notable_score():
399
+ """classify() must include 'notable_score' key."""
400
+ result = nc.classify(7)
401
+ assert "notable_score" in result
402
+ assert isinstance(result["notable_score"], int)
403
+ assert result["notable_score"] <= result["score"]
404
+
405
+
406
+ def test_classify_n1_notable_score_reasonable():
407
+ """n=1 notable_score must be much less than total score (no figurate inflation)."""
408
+ result = nc.classify(1)
409
+ # notable_score excludes figurate and figurate_centered
410
+ assert result["notable_score"] < 100
411
+ # Total score will still be large (mathematically correct)
412
+ assert result["score"] > 1000
413
+
414
+
415
+ def test_is_unique_negative():
416
+ """is_unique must return False for negative integers."""
417
+ from numclassify._core.exam_types import is_unique
418
+ assert is_unique(-5) is False
419
+ assert is_unique(-1) is False
420
+ assert is_unique(-123) is False
421
+
422
+
423
+ def test_is_practical_zero():
424
+ """is_practical(0) must return False."""
425
+ from numclassify._core.divisors import is_practical
426
+ assert is_practical(0) is False
427
+
428
+
429
+ def test_no_leaked_names():
430
+ """Optional, _version, _PackageNotFoundError must not be in dir(nc)."""
431
+ public_names = [n for n in dir(nc) if not n.startswith("__")]
432
+ assert "Optional" not in public_names
433
+ assert "_version" not in public_names
434
+ assert "_PackageNotFoundError" not in public_names
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes