plain 0.58.0__py3-none-any.whl → 0.60.0__py3-none-any.whl

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.
plain/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # plain changelog
2
2
 
3
+ ## [0.60.0](https://github.com/dropseed/plain/releases/plain@0.60.0) (2025-08-27)
4
+
5
+ ### What's changed
6
+
7
+ - Added new `APP_VERSION` setting that defaults to the project version from `pyproject.toml` ([57fb948d46](https://github.com/dropseed/plain/commit/57fb948d46))
8
+ - Updated `get_app_name_from_pyproject()` to `get_app_info_from_pyproject()` to return both name and version ([57fb948d46](https://github.com/dropseed/plain/commit/57fb948d46))
9
+
10
+ ### Upgrade instructions
11
+
12
+ - No changes required
13
+
14
+ ## [0.59.0](https://github.com/dropseed/plain/releases/plain@0.59.0) (2025-08-22)
15
+
16
+ ### What's changed
17
+
18
+ - Added new `APP_NAME` setting that defaults to the project name from `pyproject.toml` ([1a4d60e](https://github.com/dropseed/plain/commit/1a4d60e787))
19
+ - Template views now validate that `get_template_names()` returns a list instead of a string ([428a64f](https://github.com/dropseed/plain/commit/428a64f8cc))
20
+ - Object views now use cached properties for `.object` and `.objects` to improve performance ([bd0507a](https://github.com/dropseed/plain/commit/bd0507a72c))
21
+ - Improved `plain upgrade` command to suggest using subagents when there are more than 3 package updates ([497c30d](https://github.com/dropseed/plain/commit/497c30d445))
22
+
23
+ ### Upgrade instructions
24
+
25
+ - In object views, `self.load_object()` is no longer necessary as `self.object` is now a cached property.
26
+
3
27
  ## [0.58.0](https://github.com/dropseed/plain/releases/plain@0.58.0) (2025-08-19)
4
28
 
5
29
  ### What's changed
plain/cli/upgrade.py CHANGED
@@ -158,6 +158,7 @@ def build_prompt(before_after: dict[str, tuple[str | None, str | None]]) -> str:
158
158
  "",
159
159
  "3. **Available tools:**",
160
160
  " - Python shell: `uv run python`",
161
+ " - If you have a subagents feature and there are more than three packages here, use subagents",
161
162
  "",
162
163
  "4. **Workflow:**",
163
164
  " - Review changelog for each package → Apply changes → Move to next package",
@@ -3,10 +3,16 @@ Default Plain settings. Override these with settings in the module pointed to
3
3
  by the PLAIN_SETTINGS_MODULE environment variable.
4
4
  """
5
5
 
6
+ from .utils import get_app_info_from_pyproject
7
+
6
8
  # MARK: Core Settings
7
9
 
8
10
  DEBUG: bool = False
9
11
 
12
+ name, version = get_app_info_from_pyproject()
13
+ APP_NAME: str = name
14
+ APP_VERSION: str = version
15
+
10
16
  # List of strings representing installed packages.
11
17
  INSTALLED_PACKAGES: list[str] = []
12
18
 
plain/runtime/utils.py ADDED
@@ -0,0 +1,23 @@
1
+ import tomllib
2
+ from pathlib import Path
3
+
4
+
5
+ def get_app_info_from_pyproject():
6
+ """Get the project name and version from the nearest pyproject.toml file."""
7
+ current_path = Path.cwd()
8
+
9
+ # Walk up the directory tree looking for pyproject.toml
10
+ for path in [current_path] + list(current_path.parents):
11
+ pyproject_path = path / "pyproject.toml"
12
+ if pyproject_path.exists():
13
+ try:
14
+ with pyproject_path.open("rb") as f:
15
+ pyproject = tomllib.load(f)
16
+ project = pyproject.get("project", {})
17
+ name = project.get("name", "App")
18
+ version = project.get("version", "dev")
19
+ return name, version
20
+ except (tomllib.TOMLDecodeError, OSError):
21
+ continue
22
+
23
+ return "App", "dev"
plain/views/objects.py CHANGED
@@ -1,28 +1,55 @@
1
+ from functools import cached_property
2
+
1
3
  from plain.exceptions import ImproperlyConfigured, ObjectDoesNotExist
2
4
  from plain.forms import Form
3
- from plain.http import Http404, Response
5
+ from plain.http import Http404
4
6
 
5
7
  from .forms import FormView
6
8
  from .templates import TemplateView
7
9
 
8
10
 
11
+ class CreateView(FormView):
12
+ """
13
+ View for creating a new object, with a response rendered by a template.
14
+ """
15
+
16
+ # TODO? would rather you have to specify this...
17
+ def get_success_url(self, form):
18
+ """Return the URL to redirect to after processing a valid form."""
19
+ if self.success_url:
20
+ url = self.success_url.format(**self.object.__dict__)
21
+ else:
22
+ try:
23
+ url = self.object.get_absolute_url()
24
+ except AttributeError:
25
+ raise ImproperlyConfigured(
26
+ "No URL to redirect to. Either provide a url or define"
27
+ " a get_absolute_url method on the Model."
28
+ )
29
+ return url
30
+
31
+ def form_valid(self, form):
32
+ """If the form is valid, save the associated model."""
33
+ self.object = form.save()
34
+ return super().form_valid(form)
35
+
36
+
9
37
  class ObjectTemplateViewMixin:
10
38
  context_object_name = ""
11
39
 
12
- def get(self) -> Response:
13
- self.load_object()
14
- return self.render_template()
15
-
16
- def load_object(self) -> None:
40
+ @cached_property
41
+ def object(self):
17
42
  try:
18
- self.object = self.get_object()
43
+ obj = self.get_object()
19
44
  except ObjectDoesNotExist:
20
45
  raise Http404
21
46
 
22
- if not self.object:
47
+ if not obj:
23
48
  # Also raise 404 if the object is None
24
49
  raise Http404
25
50
 
51
+ return obj
52
+
26
53
  def get_object(self): # Intentionally untyped... subclasses must override this.
27
54
  raise NotImplementedError(
28
55
  f"get_object() is not implemented on {self.__class__.__name__}"
@@ -50,55 +77,9 @@ class DetailView(ObjectTemplateViewMixin, TemplateView):
50
77
  pass
51
78
 
52
79
 
53
- class CreateView(ObjectTemplateViewMixin, FormView):
54
- """
55
- View for creating a new object, with a response rendered by a template.
56
- """
57
-
58
- def post(self) -> Response:
59
- """
60
- Handle POST requests: instantiate a form instance with the passed
61
- POST variables and then check if it's valid.
62
- """
63
- # Context expects self.object to exist
64
- self.load_object()
65
- return super().post()
66
-
67
- def load_object(self) -> None:
68
- self.object = None
69
-
70
- # TODO? would rather you have to specify this...
71
- def get_success_url(self, form):
72
- """Return the URL to redirect to after processing a valid form."""
73
- if self.success_url:
74
- url = self.success_url.format(**self.object.__dict__)
75
- else:
76
- try:
77
- url = self.object.get_absolute_url()
78
- except AttributeError:
79
- raise ImproperlyConfigured(
80
- "No URL to redirect to. Either provide a url or define"
81
- " a get_absolute_url method on the Model."
82
- )
83
- return url
84
-
85
- def form_valid(self, form):
86
- """If the form is valid, save the associated model."""
87
- self.object = form.save()
88
- return super().form_valid(form)
89
-
90
-
91
80
  class UpdateView(ObjectTemplateViewMixin, FormView):
92
81
  """View for updating an object, with a response rendered by a template."""
93
82
 
94
- def post(self) -> Response:
95
- """
96
- Handle POST requests: instantiate a form instance with the passed
97
- POST variables and then check if it's valid.
98
- """
99
- self.load_object()
100
- return super().post()
101
-
102
83
  def get_success_url(self, form):
103
84
  """Return the URL to redirect to after processing a valid form."""
104
85
  if self.success_url:
@@ -115,7 +96,7 @@ class UpdateView(ObjectTemplateViewMixin, FormView):
115
96
 
116
97
  def form_valid(self, form):
117
98
  """If the form is valid, save the associated model."""
118
- self.object = form.save()
99
+ form.save()
119
100
  return super().form_valid(form)
120
101
 
121
102
  def get_form_kwargs(self):
@@ -141,14 +122,6 @@ class DeleteView(ObjectTemplateViewMixin, FormView):
141
122
 
142
123
  form_class = EmptyDeleteForm
143
124
 
144
- def post(self) -> Response:
145
- """
146
- Handle POST requests: instantiate a form instance with the passed
147
- POST variables and then check if it's valid.
148
- """
149
- self.load_object()
150
- return super().post()
151
-
152
125
  def get_form_kwargs(self):
153
126
  """Return the keyword arguments for instantiating the form."""
154
127
  kwargs = super().get_form_kwargs()
@@ -169,9 +142,9 @@ class ListView(TemplateView):
169
142
 
170
143
  context_object_name = ""
171
144
 
172
- def get(self) -> Response:
173
- self.objects = self.get_objects()
174
- return super().get()
145
+ @cached_property
146
+ def objects(self):
147
+ return self.get_objects()
175
148
 
176
149
  def get_objects(self):
177
150
  raise NotImplementedError(
plain/views/templates.py CHANGED
@@ -36,6 +36,12 @@ class TemplateView(View):
36
36
  def get_template(self) -> Template:
37
37
  template_names = self.get_template_names()
38
38
 
39
+ if isinstance(template_names, str):
40
+ raise ImproperlyConfigured(
41
+ f"{self.__class__.__name__}.get_template_names() must return a list of strings, "
42
+ f"not a string. Did you mean to return ['{template_names}']?"
43
+ )
44
+
39
45
  if not template_names:
40
46
  raise ImproperlyConfigured(
41
47
  f"{self.__class__.__name__} requires a template_name or get_template_names()."
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain
3
- Version: 0.58.0
3
+ Version: 0.60.0
4
4
  Summary: A web framework for building products with Python.
5
5
  Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
6
6
  License-File: LICENSE
@@ -1,4 +1,4 @@
1
- plain/CHANGELOG.md,sha256=NnYEqRRQ2lTPIgppmEarKtSZRoLq_N80pgXjyGf6c00,9602
1
+ plain/CHANGELOG.md,sha256=D_pNXRYALt2M76lgFw0DsNBweBA3wjnESTQycSEMz68,10968
2
2
  plain/README.md,sha256=5BJyKhf0TDanWVbOQyZ3zsi5Lov9xk-LlJYCDWofM6Y,4078
3
3
  plain/__main__.py,sha256=GK39854Lc_LO_JP8DzY9Y2MIQ4cQEl7SXFJy244-lC8,110
4
4
  plain/debug.py,sha256=XdjnXcbPGsi0J2SpHGaLthhYU5AjhBlkHdemaP4sbYY,758
@@ -37,7 +37,7 @@ plain/cli/scaffold.py,sha256=mcywA9DzfwoBSqWl5-Zpgcy1mTNUGEgdvoxXUrGcEVk,1351
37
37
  plain/cli/settings.py,sha256=9cx4bue664I2P7kUedlf4YhCPB0tSKSE4Q8mGyzEv2o,1995
38
38
  plain/cli/shell.py,sha256=iIwvlTdTBjLBBUdXMAmIRWSoynszOZI79-mrBg4RegU,1373
39
39
  plain/cli/startup.py,sha256=wLaFuyUb4ewWhtehBCGicrRCXIIGCRbeCT3ce9hUv-A,1022
40
- plain/cli/upgrade.py,sha256=f1rL9M285i4D5UKlEk4ijmAUN3mAMfHCrbDPwBf1ePo,5514
40
+ plain/cli/upgrade.py,sha256=eGVWm0gpn-Pr6uPsfzojRmh_VU5--B0h9dYfQuXSzi8,5625
41
41
  plain/cli/urls.py,sha256=ghCW36aRszxmTo06A50FIvYopb6kQ07QekkDzM6_A1o,3824
42
42
  plain/cli/utils.py,sha256=VwlIh0z7XxzVV8I3qM2kZo07fkJFPoeeVZa1ODG616k,258
43
43
  plain/csrf/README.md,sha256=ApWpB-qlEf0LkOKm9Yr-6f_lB9XJEvGFDo_fraw8ghI,2391
@@ -90,8 +90,9 @@ plain/preflight/security.py,sha256=oxUZBp2M0bpBfUoLYepIxoex2Y90nyjlrL8XU8UTHYY,2
90
90
  plain/preflight/urls.py,sha256=cQ-WnFa_5oztpKdtwhuIGb7pXEml__bHsjs1SWO2YNI,1468
91
91
  plain/runtime/README.md,sha256=sTqXXJkckwqkk9O06XMMSNRokAYjrZBnB50JD36BsYI,4873
92
92
  plain/runtime/__init__.py,sha256=8GtvKROf3HUCtneDYXTbEioPcCtwnV76dP57n2PnjuE,2343
93
- plain/runtime/global_settings.py,sha256=6vy8SacB4w4x_BAGsJv6zM3Sa7zcvnEcF2VzHfZs8qk,5645
93
+ plain/runtime/global_settings.py,sha256=LX4g0ncNif_STuM83Idcron1j_TnQ9TJwWbVywexyZo,5788
94
94
  plain/runtime/user_settings.py,sha256=OzMiEkE6ZQ50nxd1WIqirXPiNuMAQULklYHEzgzLWgA,11027
95
+ plain/runtime/utils.py,sha256=p5IuNTzc7Kq-9Ym7etYnt_xqHw5TioxfSkFeq1bKdgk,832
95
96
  plain/signals/README.md,sha256=XefXqROlDhzw7Z5l_nx6Mhq6n9jjQ-ECGbH0vvhKWYg,272
96
97
  plain/signals/__init__.py,sha256=eAs0kLqptuP6I31dWXeAqRNji3svplpAV4Ez6ktjwXM,131
97
98
  plain/signals/dispatch/__init__.py,sha256=FzEygqV9HsM6gopio7O2Oh_X230nA4d5Q9s0sUjMq0E,292
@@ -148,11 +149,11 @@ plain/views/base.py,sha256=CC9UvMZeAjVvi90vGjoZzsQ0jnhbg3-7qCKQ8-Pb6cg,4184
148
149
  plain/views/errors.py,sha256=jbNCJIzowwCsEvqyJ3opMeZpPDqTyhtrbqb0VnAm2HE,1263
149
150
  plain/views/exceptions.py,sha256=b4euI49ZUKS9O8AGAcFfiDpstzkRAuuj_uYQXzWNHME,138
150
151
  plain/views/forms.py,sha256=ESZOXuo6IeYixp1RZvPb94KplkowRiwO2eGJCM6zJI0,2400
151
- plain/views/objects.py,sha256=GGbcfg_9fPZ-PiaBwIHG2e__8GfWDR7JQtQ15wTyiHg,5970
152
+ plain/views/objects.py,sha256=YNb8MO1I99HTmQghC5nFk25TQmaB_s45K5yg5BGt4qY,5018
152
153
  plain/views/redirect.py,sha256=Xpb3cB7nZYvKgkNqcAxf9Jwm2SWcQ0u2xz4oO5M3vP8,1909
153
- plain/views/templates.py,sha256=TeVKxfyXwQTmWfWzwjtlZ8GXogcHUDZ1osc--G4YLVw,1588
154
- plain-0.58.0.dist-info/METADATA,sha256=yNuHnxpkBcNUME3wBUBvYxV-X6brEHQaXUGeMdFC3qc,4488
155
- plain-0.58.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
156
- plain-0.58.0.dist-info/entry_points.txt,sha256=nn4uKTRRZuEKOJv3810s3jtSMW0Gew7XDYiKIvBRR6M,93
157
- plain-0.58.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
158
- plain-0.58.0.dist-info/RECORD,,
154
+ plain/views/templates.py,sha256=oAlebEyfES0rzBhfyEJzFmgLkpkbleA6Eip-8zDp-yk,1863
155
+ plain-0.60.0.dist-info/METADATA,sha256=w-VNXX3R5YsTPJ_UBnFSzu33jUHnwPHAQdzMJZxILhc,4488
156
+ plain-0.60.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
157
+ plain-0.60.0.dist-info/entry_points.txt,sha256=nn4uKTRRZuEKOJv3810s3jtSMW0Gew7XDYiKIvBRR6M,93
158
+ plain-0.60.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
159
+ plain-0.60.0.dist-info/RECORD,,
File without changes