PyReprism 0.1.0__tar.gz → 0.2.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 (190) hide show
  1. {pyreprism-0.1.0 → pyreprism-0.2.0}/CHANGELOG.md +41 -1
  2. {pyreprism-0.1.0 → pyreprism-0.2.0}/PKG-INFO +77 -30
  3. {pyreprism-0.1.0 → pyreprism-0.2.0}/README.md +50 -0
  4. pyreprism-0.2.0/docs/intro.rst +100 -0
  5. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/__init__.py +44 -3
  6. pyreprism-0.2.0/src/PyReprism/_tokenops.py +153 -0
  7. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/cli.py +117 -3
  8. pyreprism-0.2.0/src/PyReprism/fingerprints.py +145 -0
  9. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/base.py +38 -0
  10. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/bro.py +1 -1
  11. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/coffeescript.py +1 -1
  12. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/eiffel.py +3 -2
  13. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/gedcom.py +1 -1
  14. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/gherkin.py +1 -1
  15. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/io.py +1 -1
  16. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/nix.py +1 -1
  17. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/smalltalk.py +7 -1
  18. pyreprism-0.2.0/src/PyReprism/metrics.py +101 -0
  19. pyreprism-0.2.0/src/PyReprism/ngrams.py +169 -0
  20. pyreprism-0.1.0/docs/intro.rst +0 -53
  21. pyreprism-0.1.0/src/PyReprism/_tokenops.py +0 -78
  22. pyreprism-0.1.0/src/PyReprism/metrics.py +0 -41
  23. {pyreprism-0.1.0 → pyreprism-0.2.0}/.flake8 +0 -0
  24. {pyreprism-0.1.0 → pyreprism-0.2.0}/.github/FUNDING.yaml +0 -0
  25. {pyreprism-0.1.0 → pyreprism-0.2.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  26. {pyreprism-0.1.0 → pyreprism-0.2.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  27. {pyreprism-0.1.0 → pyreprism-0.2.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  28. {pyreprism-0.1.0 → pyreprism-0.2.0}/.github/scripts/changelog_notes.py +0 -0
  29. {pyreprism-0.1.0 → pyreprism-0.2.0}/.github/scripts/package_version.py +0 -0
  30. {pyreprism-0.1.0 → pyreprism-0.2.0}/.github/workflows/ci.yml +0 -0
  31. {pyreprism-0.1.0 → pyreprism-0.2.0}/.github/workflows/publish.yml +0 -0
  32. {pyreprism-0.1.0 → pyreprism-0.2.0}/.gitignore +0 -0
  33. {pyreprism-0.1.0 → pyreprism-0.2.0}/.pre-commit-hooks.yaml +0 -0
  34. {pyreprism-0.1.0 → pyreprism-0.2.0}/.readthedocs.yaml +0 -0
  35. {pyreprism-0.1.0 → pyreprism-0.2.0}/CONTRIBUTING.md +0 -0
  36. {pyreprism-0.1.0 → pyreprism-0.2.0}/LICENSE +0 -0
  37. {pyreprism-0.1.0 → pyreprism-0.2.0}/RELEASING.md +0 -0
  38. {pyreprism-0.1.0 → pyreprism-0.2.0}/codecov.yml +0 -0
  39. {pyreprism-0.1.0 → pyreprism-0.2.0}/pyproject.toml +0 -0
  40. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/__main__.py +0 -0
  41. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/batch.py +0 -0
  42. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/diffs.py +0 -0
  43. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/engines/__init__.py +0 -0
  44. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/engines/base.py +0 -0
  45. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/engines/pygments_engine.py +0 -0
  46. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/engines/regex_engine.py +0 -0
  47. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/__init__.py +0 -0
  48. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/abap.py +0 -0
  49. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/actionscript.py +0 -0
  50. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/ada.py +0 -0
  51. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/apacheconf.py +0 -0
  52. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/apl.py +0 -0
  53. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/applescript.py +0 -0
  54. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/arduino.py +0 -0
  55. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/arff.py +0 -0
  56. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/asciidoc.py +0 -0
  57. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/asm6502.py +0 -0
  58. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/aspnet.py +0 -0
  59. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/autohotkey.py +0 -0
  60. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/autoit.py +0 -0
  61. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/bash.py +0 -0
  62. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/basic.py +0 -0
  63. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/batch.py +0 -0
  64. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/bison.py +0 -0
  65. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/brainfuck.py +0 -0
  66. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/c.py +0 -0
  67. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/clike.py +0 -0
  68. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/clojure.py +0 -0
  69. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/cpp.py +0 -0
  70. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/crystal.py +0 -0
  71. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/csharp.py +0 -0
  72. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/csp.py +0 -0
  73. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/css.py +0 -0
  74. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/css_extras.py +0 -0
  75. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/d.py +0 -0
  76. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/dart.py +0 -0
  77. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/diff.py +0 -0
  78. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/django.py +0 -0
  79. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/docker.py +0 -0
  80. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/elixir.py +0 -0
  81. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/erb.py +0 -0
  82. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/erlang.py +0 -0
  83. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/flow.py +0 -0
  84. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/fortran.py +0 -0
  85. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/fsharp.py +0 -0
  86. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/git.py +0 -0
  87. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/glsl.py +0 -0
  88. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/go.py +0 -0
  89. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/graphql.py +0 -0
  90. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/groovy.py +0 -0
  91. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/haml.py +0 -0
  92. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/handlebars.py +0 -0
  93. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/haskell.py +0 -0
  94. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/haxe.py +0 -0
  95. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/hpkp.py +0 -0
  96. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/hsts.py +0 -0
  97. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/html.py +0 -0
  98. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/ichigojam.py +0 -0
  99. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/icon.py +0 -0
  100. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/inform7.py +0 -0
  101. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/ini.py +0 -0
  102. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/j.py +0 -0
  103. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/java.py +0 -0
  104. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/javascript.py +0 -0
  105. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/jolie.py +0 -0
  106. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/json.py +0 -0
  107. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/jsx.py +0 -0
  108. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/julia.py +0 -0
  109. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/keyman.py +0 -0
  110. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/kotlin.py +0 -0
  111. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/latex.py +0 -0
  112. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/less.py +0 -0
  113. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/liquid.py +0 -0
  114. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/livescript.py +0 -0
  115. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/lolcode.py +0 -0
  116. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/lua.py +0 -0
  117. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/makefile.py +0 -0
  118. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/markdown.py +0 -0
  119. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/markup.py +0 -0
  120. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/markup_templating.py +0 -0
  121. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/matlab.py +0 -0
  122. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/mel.py +0 -0
  123. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/mizar.py +0 -0
  124. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/monkey.py +0 -0
  125. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/n4js.py +0 -0
  126. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/nasm.py +0 -0
  127. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/nginx.py +0 -0
  128. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/nim.py +0 -0
  129. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/nsis.py +0 -0
  130. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/objectivec.py +0 -0
  131. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/ocaml.py +0 -0
  132. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/opencl.py +0 -0
  133. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/oz.py +0 -0
  134. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/parigp.py +0 -0
  135. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/parser.py +0 -0
  136. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/pascal.py +0 -0
  137. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/perl.py +0 -0
  138. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/php.py +0 -0
  139. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/php_extras.py +0 -0
  140. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/plsql.py +0 -0
  141. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/powershell.py +0 -0
  142. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/processing.py +0 -0
  143. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/prolog.py +0 -0
  144. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/properties.py +0 -0
  145. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/protobuf.py +0 -0
  146. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/pug.py +0 -0
  147. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/puppet.py +0 -0
  148. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/pure.py +0 -0
  149. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/python.py +0 -0
  150. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/q.py +0 -0
  151. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/qore.py +0 -0
  152. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/r.py +0 -0
  153. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/reason.py +0 -0
  154. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/registry.py +0 -0
  155. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/renpy.py +0 -0
  156. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/rest.py +0 -0
  157. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/rip.py +0 -0
  158. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/roboconf.py +0 -0
  159. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/ruby.py +0 -0
  160. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/rust.py +0 -0
  161. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/sas.py +0 -0
  162. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/sass.py +0 -0
  163. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/scala.py +0 -0
  164. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/scheme.py +0 -0
  165. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/scss.py +0 -0
  166. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/smarty.py +0 -0
  167. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/soy.py +0 -0
  168. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/sql.py +0 -0
  169. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/stylus.py +0 -0
  170. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/swift.py +0 -0
  171. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/tcl.py +0 -0
  172. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/textile.py +0 -0
  173. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/tsx.py +0 -0
  174. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/twig.py +0 -0
  175. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/typescript.py +0 -0
  176. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/vbnet.py +0 -0
  177. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/velocity.py +0 -0
  178. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/verilog.py +0 -0
  179. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/vhdl.py +0 -0
  180. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/vim.py +0 -0
  181. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/visual_basic.py +0 -0
  182. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/wasam.py +0 -0
  183. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/xeora.py +0 -0
  184. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/xojo.py +0 -0
  185. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/languages/yaml.py +0 -0
  186. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/py.typed +0 -0
  187. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/tokens.py +0 -0
  188. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/utils/__init__.py +0 -0
  189. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/utils/extension.py +0 -0
  190. {pyreprism-0.1.0 → pyreprism-0.2.0}/src/PyReprism/utils/normalizer.py +0 -0
@@ -7,7 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.0] - 2026-07-08
11
+
12
+ This release adds a static code-analysis toolkit on top of the tokenizer:
13
+ diff/PR analysis, complexity metrics, n-gram "naturalness", and clone/plagiarism
14
+ similarity — plus a comprehensive per-language test suite that hardened comment
15
+ handling across the board.
16
+
10
17
  ### Added
18
+ - **Fingerprinting & similarity** (`PyReprism.fingerprints`): winnowing k-gram
19
+ fingerprints over the normalized token stream for clone/plagiarism detection —
20
+ `fingerprint()`, `similarity()`/`containment()` (rename-invariant by default),
21
+ and a `FingerprintIndex` for many-to-many detection over a corpus. CLI:
22
+ `pyreprism similarity a b` and `pyreprism clones DIR --threshold`.
23
+ - **N-gram analysis & code naturalness** (`PyReprism.ngrams`): token/type n-gram
24
+ extraction and frequency counts, plus an `NgramModel` (add-k smoothing,
25
+ save/load) that measures cross-entropy / perplexity against a trained corpus
26
+ ("naturalness of software"). CLI: `pyreprism ngrams` and
27
+ `pyreprism perplexity --train`.
28
+ - **Complexity metrics** computed from the token stream: `halstead()`
29
+ (volume/difficulty/effort/bugs), `cyclomatic_complexity()` (approximate McCabe),
30
+ `maintainability_index()` (0–100), `max_nesting_depth()`, and `code_metrics()`
31
+ which bundles them with the line/token stats. Exposed on the CLI via
32
+ `pyreprism stats --full`.
11
33
  - **Diff processing** (`PyReprism.diffs`): parse unified/`git` diffs and analyze
12
34
  the changed code per file in its own language. Includes churn metrics
13
35
  (`diff_stats`: added/removed split into code, comment and blank), cosmetic
@@ -17,6 +39,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
17
39
  `old_source` enables accurate full-file classification. New CLI command
18
40
  `pyreprism diff` (`--json` / `--csv` / `--per-file` / `--cosmetic`).
19
41
 
42
+ ### Changed
43
+ - Adopted the standard `src/` layout with tests at the top level, consolidated
44
+ all dependencies into `pyproject.toml` extras, and rewrote the landing page.
45
+ - Single-sourced the version from the package and automated releases: pushing a
46
+ `vX.Y.Z` tag now publishes to PyPI (Trusted Publishing) and creates a GitHub
47
+ Release. Added Python 3.13 to the CI matrix.
48
+ - Added a comprehensive, data-driven per-language comment test suite that
49
+ requires every registered language to be covered.
50
+
51
+ ### Fixed
52
+ - Fixed comment stripping in 8 more languages surfaced by the new test suite:
53
+ `smalltalk` (kept the comment and deleted the code) and
54
+ `eiffel`/`bro`/`coffeescript`/`io`/`nix`/`gherkin`/`gedcom` (trailing comments
55
+ not removed and newlines dropped).
56
+ - Fixed a CLI argument-ordering incompatibility on Python ≤ 3.11 (options must
57
+ follow positionals).
58
+
20
59
  ## [0.1.0] - 2026-07-07
21
60
 
22
61
  This release turns PyReprism from a comment-removal helper into a full
@@ -73,6 +112,7 @@ ML-oriented normalization, an optional accurate backend, and batch processing.
73
112
  - Early beta releases: comment removal for an initial set of languages and the
74
113
  `Normalizer` whitespace helper.
75
114
 
76
- [Unreleased]: https://github.com/unlv-evol/PyReprism/compare/v0.1.0...HEAD
115
+ [Unreleased]: https://github.com/unlv-evol/PyReprism/compare/v0.2.0...HEAD
116
+ [0.2.0]: https://github.com/unlv-evol/PyReprism/compare/v0.1.0...v0.2.0
77
117
  [0.1.0]: https://github.com/unlv-evol/PyReprism/releases/tag/v0.1.0
78
118
  [0.0.4]: https://github.com/unlv-evol/PyReprism/releases/tag/v0.0.4
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyReprism
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Framework for Source Code Preprocessing
5
5
  Project-URL: Homepage, https://github.com/unlv-evol/PyReprism
6
6
  Project-URL: Documentation, https://pyreprism.readthedocs.io
@@ -58,54 +58,101 @@ Description-Content-Type: text/x-rst
58
58
 
59
59
  .. _intro_toplevel:
60
60
 
61
- ==================
62
- Overview / Install
63
- ==================
61
+ ========
62
+ Overview
63
+ ========
64
64
 
65
- PyReprism is a Python framework that helps researchers and developers the task of source code preprocessing. With PyReprism, you can easily match, extract, count, and remove comments, whitespaces, operators, numbers and other language specific constructs from over 150 programming languages and file extensions.
65
+ **PyReprism** is a Python framework for source-code preprocessing. It lets you
66
+ **match, extract, count, and remove** comments, strings, numbers, operators,
67
+ keywords and other language-specific constructs across **145+ programming
68
+ languages and file formats** — through a small high-level API, a command-line
69
+ tool, code metrics, ML-oriented normalization, and diff/pull-request analysis.
66
70
 
71
+ Highlights
72
+ ==========
73
+
74
+ * One consistent API for every language: ``remove_*`` / ``extract_*`` /
75
+ ``count_*`` / ``match_*`` for each construct, plus a lossless ``tokenize()``.
76
+ * Code metrics (``stats``) and ML canonicalization (``normalize``) for
77
+ clone/plagiarism detection and code-embedding pipelines.
78
+ * Optional high-accuracy `Pygments`_ backend — with **zero required
79
+ dependencies** by default.
80
+ * Batch/corpus analysis and unified/``git`` diff processing.
81
+ * A ``pyreprism`` command-line tool (also ``python -m PyReprism``).
67
82
 
68
83
  Requirements
69
84
  ============
70
85
 
71
- * `Python`_ 3.6 or newer
72
- * `Git`_
86
+ * `Python`_ 3.8 or newer
73
87
 
74
- .. _Python: https://www.python.org
75
- .. _Git: https://git-scm.com/
88
+ Install
89
+ =======
76
90
 
77
- Installing PyReprism
78
- ====================
91
+ .. code-block:: shell
79
92
 
80
- Installing PyReprism is easily done using `pip`_. Assuming it is installed, just run the following from the command-line:
93
+ pip install PyReprism
81
94
 
82
- .. _pip: https://pip.pypa.io/en/latest/installing.html
95
+ For the higher-accuracy tokenizer backend:
83
96
 
84
- .. sourcecode:: none
97
+ .. code-block:: shell
85
98
 
86
- $ pip install PyReprism
99
+ pip install "PyReprism[accurate]"
87
100
 
88
- Source Code
101
+ Quick start
89
102
  ===========
90
103
 
91
- PyReprism's git repo is available on GitHub, which can be browsed at:
104
+ .. code-block:: python
105
+
106
+ import PyReprism as pr
107
+
108
+ source = """
109
+ # a comment
110
+ x = 5 + 6
111
+ print(x) # inline
112
+ """
92
113
 
93
- * https://github.com/unlv-evol/PyReprism.git
114
+ pr.remove_comments(source, lang="python") # code without comments
115
+ pr.extract_comments(source, lang="python") # ['# a comment', '# inline']
116
+ pr.count_comments(source, lang="python") # 2
94
117
 
95
- and cloned using::
118
+ ``lang`` accepts a language name (``"python"``), a file extension (``".py"``),
119
+ or a language class. Canonicalize code for machine learning, or measure it:
120
+
121
+ .. code-block:: python
122
+
123
+ pr.normalize("total = price * 42", lang="python") # 'VAR1 = VAR2 * 0'
124
+ pr.stats(source, lang="python").comment_to_code_ratio
125
+
126
+ Command line
127
+ ============
96
128
 
97
- $ git clone https://github.com/unlv-evol/PyReprism.git
98
- $ cd PyReprism
129
+ .. code-block:: shell
99
130
 
100
- Optionally (but suggested), make use of virtual environment. Therefore, before installing the requirements run::
101
-
102
- $ python3 -m venv venv
103
- $ source venv/bin/activate
131
+ pyreprism remove comments file.py
132
+ pyreprism scan myproject/ --csv # aggregate code metrics over a tree
133
+ git diff | pyreprism diff --cosmetic # flag comment/whitespace-only changes
104
134
 
105
- Install the requirements::
106
-
107
- $ pip install -r requirements.txt
135
+ Documentation
136
+ =============
108
137
 
109
- and run the tests using pytest::
138
+ Full documentation, including the API reference and the list of supported
139
+ languages, is available at https://pyreprism.readthedocs.io.
110
140
 
111
- $ pytest
141
+ Source code
142
+ ===========
143
+
144
+ PyReprism is developed on GitHub at
145
+ https://github.com/unlv-evol/PyReprism. To work on it locally:
146
+
147
+ .. code-block:: shell
148
+
149
+ git clone https://github.com/unlv-evol/PyReprism.git
150
+ cd PyReprism
151
+ python3 -m venv venv && source venv/bin/activate
152
+ pip install -e ".[dev]"
153
+ pytest
154
+
155
+ See ``CONTRIBUTING.md`` for contribution guidelines.
156
+
157
+ .. _Python: https://www.python.org
158
+ .. _Pygments: https://pygments.org
@@ -87,6 +87,56 @@ s.comment_to_code_ratio, s.comment_density
87
87
  s.as_dict() # includes per-token-type counts, ready for JSON / dataframes
88
88
  ```
89
89
 
90
+ Complexity metrics (computed from the token stream):
91
+
92
+ ```python
93
+ pr.halstead(source, lang="python").volume # Halstead volume/difficulty/effort/bugs
94
+ pr.cyclomatic_complexity(source, lang="python") # approximate McCabe complexity
95
+ pr.maintainability_index(source, lang="python") # 0–100 (higher is better)
96
+ pr.code_metrics(source, lang="python") # everything above in one dict
97
+ ```
98
+
99
+ On the CLI: `pyreprism stats --full file.py` (add `--json` for machine output).
100
+
101
+ ### Similarity & clone / plagiarism detection
102
+
103
+ Winnowing k-gram fingerprints over the *normalized* token stream, so matches
104
+ survive variable renaming and literal changes (Type-2 clones):
105
+
106
+ ```python
107
+ from PyReprism import fingerprints as fp
108
+
109
+ fp.similarity(code_a, code_b, "python") # 0.0–1.0 (Jaccard of fingerprints)
110
+ fp.containment(code_a, code_b, "python") # how much of A appears in B
111
+
112
+ index = fp.FingerprintIndex() # many-to-many clone detection
113
+ index.add_paths("submissions/")
114
+ index.similar_pairs(threshold=0.7) # -> [(file_a, file_b, score), ...]
115
+ ```
116
+
117
+ On the CLI: `pyreprism similarity a.py b.py` and
118
+ `pyreprism clones submissions/ --threshold 0.7`.
119
+
120
+ ### N-grams & code "naturalness"
121
+
122
+ Token n-grams (over token text or, structurally, over token *types*) and an
123
+ n-gram language model that measures how predictable/"natural" code is
124
+ (Hindle et al.):
125
+
126
+ ```python
127
+ from PyReprism import ngrams
128
+
129
+ ngrams.ngram_counts(source, "python", n=3).most_common(10)
130
+ ngrams.ngrams(source, "python", n=2, types=True) # structural n-grams
131
+
132
+ model = ngrams.train("corpus/", n=3) # train on a code corpus
133
+ model.perplexity(ngrams.token_sequence(source, "python")) # lower = more natural
134
+ model.save("model.json")
135
+ ```
136
+
137
+ On the CLI: `pyreprism ngrams file.py -n 3 --top 20` and
138
+ `pyreprism perplexity --train corpus/ file.py`.
139
+
90
140
  ### Normalization for ML / clone detection
91
141
 
92
142
  Canonicalize code so that only its structure remains — rename identifiers to
@@ -0,0 +1,100 @@
1
+ .. _intro_toplevel:
2
+
3
+ ========
4
+ Overview
5
+ ========
6
+
7
+ **PyReprism** is a Python framework for source-code preprocessing. It lets you
8
+ **match, extract, count, and remove** comments, strings, numbers, operators,
9
+ keywords and other language-specific constructs across **145+ programming
10
+ languages and file formats** — through a small high-level API, a command-line
11
+ tool, code metrics, ML-oriented normalization, and diff/pull-request analysis.
12
+
13
+ Highlights
14
+ ==========
15
+
16
+ * One consistent API for every language: ``remove_*`` / ``extract_*`` /
17
+ ``count_*`` / ``match_*`` for each construct, plus a lossless ``tokenize()``.
18
+ * Code metrics (``stats``) and ML canonicalization (``normalize``) for
19
+ clone/plagiarism detection and code-embedding pipelines.
20
+ * Optional high-accuracy `Pygments`_ backend — with **zero required
21
+ dependencies** by default.
22
+ * Batch/corpus analysis and unified/``git`` diff processing.
23
+ * A ``pyreprism`` command-line tool (also ``python -m PyReprism``).
24
+
25
+ Requirements
26
+ ============
27
+
28
+ * `Python`_ 3.8 or newer
29
+
30
+ Install
31
+ =======
32
+
33
+ .. code-block:: shell
34
+
35
+ pip install PyReprism
36
+
37
+ For the higher-accuracy tokenizer backend:
38
+
39
+ .. code-block:: shell
40
+
41
+ pip install "PyReprism[accurate]"
42
+
43
+ Quick start
44
+ ===========
45
+
46
+ .. code-block:: python
47
+
48
+ import PyReprism as pr
49
+
50
+ source = """
51
+ # a comment
52
+ x = 5 + 6
53
+ print(x) # inline
54
+ """
55
+
56
+ pr.remove_comments(source, lang="python") # code without comments
57
+ pr.extract_comments(source, lang="python") # ['# a comment', '# inline']
58
+ pr.count_comments(source, lang="python") # 2
59
+
60
+ ``lang`` accepts a language name (``"python"``), a file extension (``".py"``),
61
+ or a language class. Canonicalize code for machine learning, or measure it:
62
+
63
+ .. code-block:: python
64
+
65
+ pr.normalize("total = price * 42", lang="python") # 'VAR1 = VAR2 * 0'
66
+ pr.stats(source, lang="python").comment_to_code_ratio
67
+
68
+ Command line
69
+ ============
70
+
71
+ .. code-block:: shell
72
+
73
+ pyreprism remove comments file.py
74
+ pyreprism scan myproject/ --csv # aggregate code metrics over a tree
75
+ git diff | pyreprism diff --cosmetic # flag comment/whitespace-only changes
76
+
77
+ Documentation
78
+ =============
79
+
80
+ Full documentation, including the API reference and the list of supported
81
+ languages, is available at https://pyreprism.readthedocs.io.
82
+
83
+ Source code
84
+ ===========
85
+
86
+ PyReprism is developed on GitHub at
87
+ https://github.com/unlv-evol/PyReprism. To work on it locally:
88
+
89
+ .. code-block:: shell
90
+
91
+ git clone https://github.com/unlv-evol/PyReprism.git
92
+ cd PyReprism
93
+ python3 -m venv venv && source venv/bin/activate
94
+ pip install -e ".[dev]"
95
+ pytest
96
+
97
+ See ``CONTRIBUTING.md`` for contribution guidelines.
98
+
99
+ .. _Python: https://www.python.org
100
+ .. _Pygments: https://pygments.org
@@ -17,11 +17,11 @@ import importlib
17
17
  import os
18
18
  from typing import List, Optional, Sequence, Type, Union
19
19
 
20
- from .metrics import CodeStats
20
+ from .metrics import CodeStats, Halstead
21
21
  from .tokens import Token, TokenType
22
22
  from .utils.normalizer import Normalizer
23
23
 
24
- __version__ = "0.1.0"
24
+ __version__ = "0.2.0"
25
25
 
26
26
  LanguageLike = Union[str, "type"]
27
27
 
@@ -274,6 +274,46 @@ def stats(source: str, lang: LanguageLike, engine: str = 'regex') -> CodeStats:
274
274
  return _tokenops.stats(_engine_tokens(source, lang, engine), source)
275
275
 
276
276
 
277
+ def halstead(source: str, lang: LanguageLike, engine: str = 'regex') -> Halstead:
278
+ """Return the :class:`Halstead` complexity measures for ``source``."""
279
+ if _is_regex(engine):
280
+ return _resolve(lang).halstead(source)
281
+ from . import _tokenops
282
+ return _tokenops.halstead(_engine_tokens(source, lang, engine))
283
+
284
+
285
+ def cyclomatic_complexity(source: str, lang: LanguageLike, engine: str = 'regex') -> int:
286
+ """Approximate McCabe cyclomatic complexity (token-based)."""
287
+ if _is_regex(engine):
288
+ return _resolve(lang).cyclomatic_complexity(source)
289
+ from . import _tokenops
290
+ return _tokenops.cyclomatic(_engine_tokens(source, lang, engine))
291
+
292
+
293
+ def maintainability_index(source: str, lang: LanguageLike, engine: str = 'regex') -> float:
294
+ """SEI-normalized Maintainability Index in ``[0, 100]`` (higher is better)."""
295
+ if _is_regex(engine):
296
+ return _resolve(lang).maintainability_index(source)
297
+ from . import _tokenops
298
+ tokens = _engine_tokens(source, lang, engine)
299
+ return _tokenops.maintainability_index(tokens, _tokenops.stats(tokens, source).code_lines)
300
+
301
+
302
+ def code_metrics(source: str, lang: LanguageLike, engine: str = 'regex') -> dict:
303
+ """Return a combined metrics dict (line stats + Halstead + complexity + MI)."""
304
+ if _is_regex(engine):
305
+ return _resolve(lang).code_metrics(source)
306
+ from . import _tokenops
307
+ tokens = _engine_tokens(source, lang, engine)
308
+ stats_obj = _tokenops.stats(tokens, source)
309
+ data = stats_obj.as_dict()
310
+ data['halstead'] = _tokenops.halstead(tokens).as_dict()
311
+ data['cyclomatic_complexity'] = _tokenops.cyclomatic(tokens)
312
+ data['max_nesting_depth'] = _tokenops.max_nesting_depth(tokens)
313
+ data['maintainability_index'] = _tokenops.maintainability_index(tokens, stats_obj.code_lines)
314
+ return data
315
+
316
+
277
317
  def normalize(source: str, lang: LanguageLike, engine: str = 'regex', **options) -> str:
278
318
  """Return a canonicalized form of ``source`` for ML / clone detection.
279
319
 
@@ -329,7 +369,7 @@ def preprocess(source: str, lang: LanguageLike, steps: Sequence[str] = ('comment
329
369
 
330
370
  __all__ = [
331
371
  '__version__',
332
- 'Token', 'TokenType', 'CodeStats', 'Normalizer',
372
+ 'Token', 'TokenType', 'CodeStats', 'Halstead', 'Normalizer',
333
373
  'get_language', 'detect_language',
334
374
  'remove_comments', 'extract_comments', 'count_comments', 'match_comments',
335
375
  'remove_keywords', 'extract_keywords', 'count_keywords',
@@ -338,5 +378,6 @@ __all__ = [
338
378
  'remove_strings', 'extract_strings',
339
379
  'extract_identifiers', 'remove_whitespaces',
340
380
  'blank_comments', 'stats', 'normalize',
381
+ 'halstead', 'cyclomatic_complexity', 'maintainability_index', 'code_metrics',
341
382
  'tokenize', 'preprocess',
342
383
  ]
@@ -0,0 +1,153 @@
1
+ """Generic operations derived from a token stream.
2
+
3
+ These work on any ``List[Token]`` regardless of which engine produced it, so the
4
+ regex and pygments backends share one implementation of remove/extract/count,
5
+ normalization and metrics.
6
+ """
7
+ import math
8
+ from collections import Counter
9
+ from typing import List, Set
10
+
11
+ from .metrics import CodeStats, Halstead
12
+ from .tokens import Token, TokenType
13
+
14
+ # Language-agnostic decision points used for the approximate cyclomatic complexity.
15
+ DECISION_KEYWORDS = frozenset({
16
+ 'if', 'elif', 'elseif', 'for', 'foreach', 'while', 'case', 'when', 'catch',
17
+ 'except', 'and', 'or', 'unless',
18
+ })
19
+ # Substrings within operator tokens that add a decision (&&, ||, ternary ?).
20
+ DECISION_OPERATORS = ('&&', '||', '?')
21
+
22
+ _OPERAND_TYPES = (TokenType.IDENTIFIER, TokenType.NUMBER, TokenType.STRING)
23
+ _OPEN_BRACKETS, _CLOSE_BRACKETS = set('([{'), set(')]}')
24
+
25
+
26
+ def remove(tokens: List[Token], ttype: TokenType) -> str:
27
+ return ''.join(t.value for t in tokens if t.type is not ttype)
28
+
29
+
30
+ def extract(tokens: List[Token], ttype: TokenType) -> List[str]:
31
+ return [t.value for t in tokens if t.type is ttype]
32
+
33
+
34
+ def count(tokens: List[Token], ttype: TokenType) -> int:
35
+ return sum(1 for t in tokens if t.type is ttype)
36
+
37
+
38
+ def normalize(tokens: List[Token], *, rename_identifiers: bool = True,
39
+ mask_numbers: bool = True, mask_strings: bool = True,
40
+ drop_comments: bool = True, collapse_whitespace: bool = False,
41
+ identifier_prefix: str = 'VAR', number_placeholder: str = '0',
42
+ string_placeholder: str = '"STR"') -> str:
43
+ mapping = {}
44
+ parts = []
45
+ for tok in tokens:
46
+ t = tok.type
47
+ if t is TokenType.COMMENT:
48
+ if not drop_comments:
49
+ parts.append(tok.value)
50
+ elif t is TokenType.STRING and mask_strings:
51
+ parts.append(string_placeholder)
52
+ elif t is TokenType.NUMBER and mask_numbers:
53
+ parts.append(number_placeholder)
54
+ elif t is TokenType.IDENTIFIER and rename_identifiers:
55
+ name = mapping.get(tok.value)
56
+ if name is None:
57
+ name = f"{identifier_prefix}{len(mapping) + 1}"
58
+ mapping[tok.value] = name
59
+ parts.append(name)
60
+ elif t is TokenType.WHITESPACE and collapse_whitespace:
61
+ parts.append(' ')
62
+ else:
63
+ parts.append(tok.value)
64
+ return ''.join(parts)
65
+
66
+
67
+ def stats(tokens: List[Token], source: str) -> CodeStats:
68
+ counts = {t: 0 for t in TokenType}
69
+ code_lines = set()
70
+ comment_lines = set()
71
+ for tok in tokens:
72
+ counts[tok.type] += 1
73
+ if tok.type is TokenType.WHITESPACE:
74
+ continue
75
+ span = range(tok.line, tok.line + tok.value.count('\n') + 1)
76
+ target = comment_lines if tok.type is TokenType.COMMENT else code_lines
77
+ target.update(span)
78
+ total = len(source.splitlines())
79
+ code = len(code_lines)
80
+ comment = len(comment_lines - code_lines)
81
+ blank = max(total - code - comment, 0)
82
+ return CodeStats(
83
+ lines=total, code_lines=code, comment_lines=comment, blank_lines=blank,
84
+ characters=len(source),
85
+ comment_tokens=counts[TokenType.COMMENT],
86
+ string_tokens=counts[TokenType.STRING],
87
+ number_tokens=counts[TokenType.NUMBER],
88
+ keyword_tokens=counts[TokenType.KEYWORD],
89
+ identifier_tokens=counts[TokenType.IDENTIFIER],
90
+ operator_tokens=counts[TokenType.OPERATOR],
91
+ )
92
+
93
+
94
+ def halstead(tokens: List[Token]) -> Halstead:
95
+ """Compute :class:`~PyReprism.metrics.Halstead` measures from ``tokens``."""
96
+ operators: Counter = Counter()
97
+ operands: Counter = Counter()
98
+ for tok in tokens:
99
+ if tok.type in (TokenType.OPERATOR, TokenType.KEYWORD):
100
+ operators[tok.value] += 1
101
+ elif tok.type is TokenType.OTHER and tok.value.strip():
102
+ operators[tok.value] += 1
103
+ elif tok.type in _OPERAND_TYPES:
104
+ operands[tok.value] += 1
105
+ return Halstead(
106
+ distinct_operators=len(operators), distinct_operands=len(operands),
107
+ total_operators=sum(operators.values()), total_operands=sum(operands.values()),
108
+ )
109
+
110
+
111
+ def cyclomatic(tokens: List[Token], decision_keywords: Set[str] = DECISION_KEYWORDS) -> int:
112
+ """Approximate McCabe cyclomatic complexity: ``1 + decision points``.
113
+
114
+ Counts branch keywords plus ``&&``/``||``/``?`` operators. This is a
115
+ token-level approximation; an exact value needs a control-flow graph.
116
+ """
117
+ complexity = 1
118
+ for tok in tokens:
119
+ # Branch words are usually KEYWORD, but word-operators (and/or) can land
120
+ # as IDENTIFIER in the generic lexer, so check both.
121
+ if (tok.type in (TokenType.KEYWORD, TokenType.IDENTIFIER)
122
+ and tok.value.lower() in decision_keywords):
123
+ complexity += 1
124
+ elif tok.type is TokenType.OPERATOR:
125
+ for op in DECISION_OPERATORS:
126
+ complexity += tok.value.count(op)
127
+ return complexity
128
+
129
+
130
+ def max_nesting_depth(tokens: List[Token]) -> int:
131
+ """Maximum bracket nesting depth (``()``/``[]``/``{}``), ignoring strings/comments."""
132
+ depth = deepest = 0
133
+ for tok in tokens:
134
+ if tok.type in (TokenType.STRING, TokenType.COMMENT):
135
+ continue
136
+ for ch in tok.value:
137
+ if ch in _OPEN_BRACKETS:
138
+ depth += 1
139
+ deepest = max(deepest, depth)
140
+ elif ch in _CLOSE_BRACKETS:
141
+ depth = max(0, depth - 1)
142
+ return deepest
143
+
144
+
145
+ def maintainability_index(tokens: List[Token], code_lines: int) -> float:
146
+ """SEI-normalized Maintainability Index in ``[0, 100]`` (higher is better)."""
147
+ volume = halstead(tokens).volume
148
+ complexity = cyclomatic(tokens)
149
+ raw = (171
150
+ - 5.2 * math.log(max(volume, 1))
151
+ - 0.23 * complexity
152
+ - 16.2 * math.log(max(code_lines, 1)))
153
+ return round(max(0.0, min(100.0, raw * 100 / 171)), 2)