Skip to content

Commit ced1c19

Browse files
committed
Add support for precision specifiers
1 parent f3284fd commit ced1c19

File tree

4 files changed

+46
-21
lines changed

4 files changed

+46
-21
lines changed

README.rst

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
Django Dynamic Filenames
33
========================
44

5-
Write advanced filename patterns using the `Format Specification Mini-Language`__.
5+
Write advanced filename patterns using the `Format String Syntax`__.
66

77
__ https://docs.python.org/3/library/string.html#format-string-syntax
88

@@ -90,6 +90,10 @@ Field names
9090

9191
.. warning:: Not all file systems support Base64 file names.
9292

93+
All type specifiers also support precisions to cut the string,
94+
e.g. ``{{uuid:.2base32}}`` would only return the first 2 characters of a
95+
Base32 encoded UUID.
96+
9397
Type specifiers
9498
~~~~~~~~~~~~~~~
9599

@@ -103,10 +107,12 @@ Example:
103107
from dynamic_names import FilePattern
104108
105109
upload_to_pattern = FilePattern(
106-
filename_pattern='{app_name:.25}/{model_name:.30}/{instance.title:slug}{ext}'
110+
filename_pattern='{app_name:.25}/{model_name:.30}/{instance.title:.40slug}{ext}'
107111
)
108112
109113
class FileModel(models.Model):
110114
title = models.CharField(max_length=100)
111115
my_file = models.FileField(upload_to=upload_to_pattern)
112116
117+
Slug type specifiers also support precisions to cut the string. In the example
118+
above the slug of the instance title will be cut at 40 characters.

dynamic_filenames.py

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import base64
22
import os
3+
import re
34
import uuid
45
from string import Formatter
56

@@ -10,40 +11,48 @@
1011

1112

1213
class SlugFormatter(Formatter):
14+
format_spec_pattern = re.compile(r'(\.\d+)?([\d\w]+)?')
1315

1416
def format_field(self, value, format_spec):
15-
if format_spec == 'slug':
16-
return slugify(value)
17+
precision, ftype = self.format_spec_pattern.match(format_spec).groups()
18+
if precision:
19+
precision = int(precision.lstrip('.'))
20+
if ftype == 'slug':
21+
return slugify(value)[:precision]
1722
return super().format_field(value=value, format_spec=format_spec)
1823

1924

2025
class ExtendedUUID(uuid.UUID):
26+
format_spec_pattern = re.compile(r'(\.\d+)?([\d\w]+)?')
2127

2228
def __format__(self, format_spec):
23-
if format_spec == '':
24-
return str(self)
25-
if format_spec == 's':
26-
return str(self)
27-
if format_spec == 'i':
28-
return str(self.int)
29-
if format_spec == 'x':
30-
return self.hex.lower()
31-
if format_spec == 'X':
32-
return self.hex.upper()
33-
if format_spec == 'base32':
29+
precision, ftype = self.format_spec_pattern.match(format_spec).groups()
30+
if precision:
31+
precision = int(precision.lstrip('.'))
32+
if ftype == '':
33+
return str(self)[:precision]
34+
if ftype == 's':
35+
return str(self)[:precision]
36+
if ftype == 'i':
37+
return str(self.int)[:precision]
38+
if ftype == 'x':
39+
return self.hex.lower()[:precision]
40+
if ftype == 'X':
41+
return self.hex.upper()[:precision]
42+
if ftype == 'base32':
3443
return base64.b32encode(
3544
self.bytes
36-
).decode('utf-8').rstrip('=\n')
37-
if format_spec == 'base64':
45+
).decode('utf-8').rstrip('=\n')[:precision]
46+
if ftype == 'base64':
3847
return base64.urlsafe_b64encode(
3948
self.bytes
40-
).decode('utf-8').rstrip('=\n')
49+
).decode('utf-8').rstrip('=\n')[:precision]
4150
return super().__format__(format_spec)
4251

4352

4453
class FilePattern:
4554
"""
46-
Write advanced filename patterns using the Format Specification Mini-Language.
55+
Write advanced filename patterns using the Format String Syntax.
4756
4857
Basic example:
4958
@@ -52,7 +61,7 @@ class FilePattern:
5261
from django.db import models
5362
from dynamic_names import FilePattern
5463
55-
upload_to_pattern = FilePattern('{app_name:.25}/{model_name:.30}/{uuid:base32}{ext}')
64+
upload_to_pattern = FilePattern('{app_name:.25}/{model_name:.30}/{uuid:.30base32}{ext}')
5665
5766
class FileModel(models.Model):
5867
my_file = models.FileField(upload_to=upload_to_pattern)

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
name = django-dynamic-filenames
33
author = Johannes Hoppe
44
author-email = info@johanneshoppe.com
5-
summary = Write advanced filename patterns using the Format Specification Mini-Language.
5+
summary = Write advanced filename patterns using the Format String Syntax.
66
description-file = README.rst
77
description-content-type = text/x-rst; charset=UTF-8
88
home-page = https://github.com/codingjoe/django-dynamic-filenames

tests/test_dynamic_filenames.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,11 @@ def test_call__slug(self):
103103
instance=DefaultModel(title='best model'), filename='some_file.txt'
104104
) == 'best-model.txt'
105105

106+
def test_call__slug_precision(self):
107+
assert FilePattern(filename_pattern='{instance.title:.4slug}{ext}')(
108+
instance=DefaultModel(title='best model'), filename='some_file.txt'
109+
) == 'best.txt'
110+
106111
def test_destruct(self):
107112
assert FilePattern().deconstruct() == ('dynamic_filenames.FilePattern', [], {})
108113
assert FilePattern(filename_pattern='{name}{ext}').deconstruct() == (
@@ -158,6 +163,11 @@ def test_uuid__super(self):
158163
format(guid, 'does not exist')
159164
assert 'unsupported format string passed to ExtendedUUID.__format__' in str(e)
160165

166+
def test_precision(self):
167+
guid = ExtendedUUID('522d6f3519204b0fb82ae8f558af2749')
168+
assert len(format(guid, '.11base64')) == 11
169+
assert format(guid, '.11base64') == 'Ui1vNRkgSw-'
170+
161171

162172
def test_migrations(db):
163173
"""Integration tests for deconstruct method."""

0 commit comments

Comments
 (0)