easyfuncplot 0.1.1__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 aicodebearcm1
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,30 @@
1
+ Metadata-Version: 2.4
2
+ Name: easyfuncplot
3
+ Version: 0.1.1
4
+ Summary: Plot mathematical functions from strings or Python callables
5
+ Author-email: Your Name <you@example.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/yourname/mathplot
8
+ Keywords: math,plot,function,matplotlib
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.9
13
+ Description-Content-Type: text/markdown
14
+ License-File: License
15
+ Requires-Dist: matplotlib>=3.5
16
+ Requires-Dist: numpy>=1.21
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest; extra == "dev"
19
+ Requires-Dist: build; extra == "dev"
20
+ Requires-Dist: twine; extra == "dev"
21
+ Dynamic: license-file
22
+
23
+ # mathplot
24
+
25
+ Plot mathematical functions from strings or Python callables.
26
+
27
+ ## 安装
28
+
29
+ ```bash
30
+ pip install easyfuncplot
@@ -0,0 +1,8 @@
1
+ # mathplot
2
+
3
+ Plot mathematical functions from strings or Python callables.
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ pip install easyfuncplot
@@ -0,0 +1,31 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "easyfuncplot"
7
+ version = "0.1.1"
8
+ description = "Plot mathematical functions from strings or Python callables"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Your Name", email = "you@example.com" }]
13
+ keywords = ["math", "plot", "function", "matplotlib"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ ]
19
+ dependencies = [
20
+ "matplotlib>=3.5",
21
+ "numpy>=1.21",
22
+ ]
23
+
24
+ [project.optional-dependencies]
25
+ dev = ["pytest", "build", "twine"]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/yourname/mathplot"
29
+
30
+ [tool.setuptools.packages.find]
31
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,30 @@
1
+ Metadata-Version: 2.4
2
+ Name: easyfuncplot
3
+ Version: 0.1.1
4
+ Summary: Plot mathematical functions from strings or Python callables
5
+ Author-email: Your Name <you@example.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/yourname/mathplot
8
+ Keywords: math,plot,function,matplotlib
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.9
13
+ Description-Content-Type: text/markdown
14
+ License-File: License
15
+ Requires-Dist: matplotlib>=3.5
16
+ Requires-Dist: numpy>=1.21
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest; extra == "dev"
19
+ Requires-Dist: build; extra == "dev"
20
+ Requires-Dist: twine; extra == "dev"
21
+ Dynamic: license-file
22
+
23
+ # mathplot
24
+
25
+ Plot mathematical functions from strings or Python callables.
26
+
27
+ ## 安装
28
+
29
+ ```bash
30
+ pip install easyfuncplot
@@ -0,0 +1,14 @@
1
+ License
2
+ README.md
3
+ pyproject.toml
4
+ src/easyfuncplot.egg-info/PKG-INFO
5
+ src/easyfuncplot.egg-info/SOURCES.txt
6
+ src/easyfuncplot.egg-info/dependency_links.txt
7
+ src/easyfuncplot.egg-info/requires.txt
8
+ src/easyfuncplot.egg-info/top_level.txt
9
+ src/mathplot/_init_.py
10
+ src/mathplot/auto_range.py
11
+ src/mathplot/parser.py
12
+ src/mathplot/plotter.py
13
+ src/mathplot/sampler.py
14
+ tests/test_basic.py
@@ -0,0 +1,7 @@
1
+ matplotlib>=3.5
2
+ numpy>=1.21
3
+
4
+ [dev]
5
+ pytest
6
+ build
7
+ twine
@@ -0,0 +1,5 @@
1
+ from .parser import parse
2
+ from .plotter import plot
3
+
4
+ __version__ = "0.1.0"
5
+ __all__ = ["parse", "plot"]
@@ -0,0 +1,18 @@
1
+ """自动 Y 轴范围:用分位数避免单个尖峰把图压扁。"""
2
+ import numpy as np
3
+ from typing import Tuple
4
+
5
+
6
+ def auto_ylim(y: np.ndarray, percentile: float = 99.5) -> Tuple[float, float]:
7
+ finite = y[np.isfinite(y)]
8
+ if finite.size == 0:
9
+ return -1.0, 1.0
10
+
11
+ lo = float(np.percentile(finite, 100 - percentile))
12
+ hi = float(np.percentile(finite, percentile))
13
+
14
+ if hi <= lo:
15
+ hi, lo = lo + 1.0, lo - 1.0
16
+
17
+ pad = (hi - lo) * 0.1
18
+ return lo - pad, hi + pad
@@ -0,0 +1,91 @@
1
+ """把字符串表达式解析成单变量函数 f(x),不使用 eval,安全可控。"""
2
+ import ast
3
+ import math
4
+ import operator
5
+ from typing import Callable
6
+
7
+ _ALLOWED_FUNCS = {
8
+ "sin": math.sin, "cos": math.cos, "tan": math.tan,
9
+ "asin": math.asin, "acos": math.acos, "atan": math.atan,
10
+ "sinh": math.sinh, "cosh": math.cosh, "tanh": math.tanh,
11
+ "exp": math.exp, "log": math.log, "log10": math.log10,
12
+ "sqrt": math.sqrt, "abs": abs,
13
+ "floor": math.floor, "ceil": math.ceil,
14
+ "sign": lambda v: (v > 0) - (v < 0),
15
+ }
16
+
17
+ _ALLOWED_CONSTS = {"pi": math.pi, "e": math.e, "tau": math.tau}
18
+
19
+ _BIN_OPS = {
20
+ ast.Add: operator.add,
21
+ ast.Sub: operator.sub,
22
+ ast.Mult: operator.mul,
23
+ ast.Div: operator.truediv,
24
+ ast.Pow: operator.pow,
25
+ ast.Mod: operator.mod,
26
+ ast.FloorDiv: operator.floordiv,
27
+ }
28
+
29
+ _UNARY_OPS = {ast.UAdd: operator.pos, ast.USub: operator.neg}
30
+
31
+
32
+ def _preprocess(expr: str) -> str:
33
+ """把用户习惯的 ^ 转成 Python 的 **。"""
34
+ return expr.replace("^", "**")
35
+
36
+
37
+ def parse(expr: str) -> Callable[[float], float]:
38
+ """把字符串表达式编译成单变量函数 f(x)。
39
+
40
+ >>> f = parse("x**2 + 1")
41
+ >>> f(3)
42
+ 10
43
+ """
44
+ if not isinstance(expr, str):
45
+ raise TypeError("expr 必须是字符串")
46
+
47
+ tree = ast.parse(_preprocess(expr), mode="eval")
48
+
49
+ def _eval(node, x):
50
+ if isinstance(node, ast.Expression):
51
+ return _eval(node.body, x)
52
+
53
+ if isinstance(node, ast.Constant):
54
+ if isinstance(node.value, (int, float)):
55
+ return node.value
56
+ raise ValueError(f"不支持的常量: {node.value!r}")
57
+
58
+ if isinstance(node, ast.Name):
59
+ if node.id == "x":
60
+ return x
61
+ if node.id in _ALLOWED_CONSTS:
62
+ return _ALLOWED_CONSTS[node.id]
63
+ raise ValueError(f"未知变量: {node.id}")
64
+
65
+ if isinstance(node, ast.BinOp):
66
+ op = _BIN_OPS.get(type(node.op))
67
+ if op is None:
68
+ raise ValueError(f"不支持的运算符: {type(node.op).__name__}")
69
+ return op(_eval(node.left, x), _eval(node.right, x))
70
+
71
+ if isinstance(node, ast.UnaryOp):
72
+ op = _UNARY_OPS.get(type(node.op))
73
+ if op is None:
74
+ raise ValueError(f"不支持的一元运算符: {type(node.op).__name__}")
75
+ return op(_eval(node.operand, x))
76
+
77
+ if isinstance(node, ast.Call):
78
+ if not isinstance(node.func, ast.Name):
79
+ raise ValueError("只支持直接调用函数名")
80
+ fn = _ALLOWED_FUNCS.get(node.func.id)
81
+ if fn is None:
82
+ raise ValueError(f"未知函数: {node.func.id}")
83
+ args = [_eval(a, x) for a in node.args]
84
+ return fn(*args)
85
+
86
+ raise ValueError(f"不支持的语法: {type(node).__name__}")
87
+
88
+ def f(x: float) -> float:
89
+ return _eval(tree, x)
90
+
91
+ return f
@@ -0,0 +1,83 @@
1
+ """渲染:把函数画成图。"""
2
+ from typing import Callable, Optional, Union
3
+
4
+ import matplotlib.pyplot as plt
5
+ import numpy as np
6
+
7
+ from .auto_range import auto_ylim
8
+ from .parser import parse
9
+ from .sampler import break_asymptotes, sample
10
+
11
+ FuncLike = Union[str, Callable[[float], float]]
12
+
13
+
14
+ def _to_callable(f: FuncLike) -> Callable[[float], float]:
15
+ if isinstance(f, str):
16
+ return parse(f)
17
+ if callable(f):
18
+ return f
19
+ raise TypeError("函数必须是字符串表达式或可调用对象")
20
+
21
+
22
+ def _label_of(f: FuncLike) -> Optional[str]:
23
+ if isinstance(f, str):
24
+ return f
25
+ return getattr(f, "__name__", None)
26
+
27
+
28
+ def plot(
29
+ *funcs: FuncLike,
30
+ xmin: float = -10,
31
+ xmax: float = 10,
32
+ n: int = 2000,
33
+ ylim: Optional[tuple] = None,
34
+ title: Optional[str] = None,
35
+ grid: bool = True,
36
+ show: bool = True,
37
+ save: Optional[str] = None,
38
+ ):
39
+ """画一个或多个函数。
40
+
41
+ 例:
42
+ plot("x**2")
43
+ plot("sin(x)", "cos(x)", xmin=-6.28, xmax=6.28)
44
+ plot(lambda x: x**3 - 3*x)
45
+ """
46
+ if not funcs:
47
+ raise ValueError("至少传入一个函数")
48
+
49
+ fig, ax = plt.subplots(figsize=(8, 5))
50
+ all_y = []
51
+
52
+ for f in funcs:
53
+ fn = _to_callable(f)
54
+ x, y = sample(fn, xmin, xmax, n)
55
+ y = break_asymptotes(y)
56
+ ax.plot(x, y, linewidth=1.6, label=_label_of(f))
57
+ all_y.append(y)
58
+
59
+ if ylim is not None:
60
+ ax.set_ylim(*ylim)
61
+ elif all_y:
62
+ ax.set_ylim(*auto_ylim(np.concatenate(all_y)))
63
+
64
+ ax.set_xlim(xmin, xmax)
65
+ ax.axhline(0, color="black", linewidth=0.6)
66
+ ax.axvline(0, color="black", linewidth=0.6)
67
+ ax.grid(grid, alpha=0.3)
68
+ ax.set_xlabel("x")
69
+ ax.set_ylabel("y")
70
+
71
+ if title:
72
+ ax.set_title(title)
73
+
74
+ # 只在有字符串表达式(或显式命名)时显示图例
75
+ if any(_label_of(f) for f in funcs):
76
+ ax.legend()
77
+
78
+ if save:
79
+ fig.savefig(save, dpi=150, bbox_inches="tight")
80
+ if show:
81
+ plt.show()
82
+
83
+ return fig, ax
@@ -0,0 +1,40 @@
1
+ """采样:把连续函数离散化,处理定义域和奇点。"""
2
+ import numpy as np
3
+ from typing import Callable, Tuple
4
+
5
+
6
+ def sample(
7
+ f: Callable[[float], float],
8
+ xmin: float,
9
+ xmax: float,
10
+ n: int = 2000,
11
+ ) -> Tuple[np.ndarray, np.ndarray]:
12
+ """在 [xmin, xmax] 上采样 n 个点,无效点返回 nan。"""
13
+ if xmax <= xmin:
14
+ raise ValueError("xmax 必须大于 xmin")
15
+ if n < 2:
16
+ raise ValueError("n 至少为 2")
17
+
18
+ x = np.linspace(xmin, xmax, n)
19
+ y = np.empty_like(x)
20
+
21
+ for i, xi in enumerate(x):
22
+ try:
23
+ y[i] = float(f(float(xi)))
24
+ except (ValueError, ZeroDivisionError, OverflowError):
25
+ y[i] = np.nan
26
+
27
+ # inf / -inf 也变成 nan,让 matplotlib 自动断开
28
+ y = np.where(np.isfinite(y), y, np.nan)
29
+ return x, y
30
+
31
+
32
+ def break_asymptotes(y: np.ndarray, threshold: float = 1e6) -> np.ndarray:
33
+ """在相邻两点数值突变处插入 nan,避免 1/x 之类的假竖线。"""
34
+ y = y.copy()
35
+ for i in range(1, len(y)):
36
+ if np.isnan(y[i - 1]) or np.isnan(y[i]):
37
+ continue
38
+ if abs(y[i] - y[i - 1]) > threshold:
39
+ y[i - 1] = np.nan
40
+ return y
@@ -0,0 +1,49 @@
1
+ import math
2
+
3
+ import pytest
4
+
5
+ from mathplot import parse, plot
6
+ from mathplot.parser import parse as parse_only
7
+
8
+
9
+ def test_parse_basic():
10
+ f = parse_only("x**2 + 1")
11
+ assert f(3) == 10
12
+
13
+
14
+ def test_parse_caret():
15
+ f = parse_only("x^2")
16
+ assert f(3) == 9
17
+
18
+
19
+ def test_parse_constants():
20
+ f = parse_only("sin(pi/2)")
21
+ assert abs(f(0) - 1.0) < 1e-12
22
+
23
+
24
+ def test_parse_rejects_unknown():
25
+ with pytest.raises(ValueError):
26
+ parse_only("__import__('os')")
27
+
28
+
29
+ def test_parse_rejects_unknown_var():
30
+ with pytest.raises(ValueError):
31
+ parse_only("y + 1")
32
+
33
+
34
+ def test_sample_handles_division_by_zero():
35
+ from mathplot.sampler import sample
36
+ f = parse_only("1/x")
37
+ x, y = sample(f, -1, 1, n=101)
38
+ # x=0 处应该是 nan
39
+ assert any(math.isnan(v) for v in y)
40
+
41
+
42
+ def test_plot_returns_fig(monkeypatch):
43
+ import matplotlib
44
+ matplotlib.use("Agg")
45
+ fig, ax = plot("x**2", show=False)
46
+ assert fig is not None
47
+ assert ax is not None
48
+ import matplotlib.pyplot as plt
49
+ plt.close(fig)