0
# Data Objects
1
2
Field and File objects for handling parsed form data with configurable storage options and lifecycle management. These objects provide the data layer for storing and accessing parsed form fields and uploaded files with automatic memory management.
3
4
## Capabilities
5
6
### Field Class
7
8
Represents a parsed form field with name and value, providing simple storage for text-based form data.
9
10
```python { .api }
11
class Field:
12
"""
13
Represents a form field with name and value.
14
"""
15
16
def __init__(self, name: bytes | None):
17
"""
18
Initialize Field object.
19
20
Parameters:
21
- name: Field name as bytes
22
"""
23
24
@classmethod
25
def from_value(cls, name: bytes, value: bytes | None) -> 'Field':
26
"""
27
Create Field from name and value.
28
29
Parameters:
30
- name: Field name as bytes
31
- value: Field value as bytes or None
32
33
Returns:
34
Field instance
35
"""
36
37
def write(self, data: bytes) -> int:
38
"""
39
Write data to field value.
40
41
Parameters:
42
- data: Bytes to append to field value
43
44
Returns:
45
Number of bytes written
46
"""
47
48
def on_data(self, data: bytes) -> int:
49
"""Handle data callback."""
50
51
def on_end(self) -> None:
52
"""Handle end callback."""
53
54
def finalize(self) -> None:
55
"""Finalize field processing."""
56
57
def close(self) -> None:
58
"""Close field and clean up resources."""
59
60
def set_none(self) -> None:
61
"""Set field value to None."""
62
63
def __eq__(self, other: object) -> bool:
64
"""Compare fields for equality."""
65
66
def __repr__(self) -> str:
67
"""String representation of field."""
68
69
# Properties
70
@property
71
def field_name(self) -> bytes | None:
72
"""This property returns the name of the field."""
73
74
@property
75
def value(self) -> bytes | None:
76
"""This property returns the value of the form field."""
77
```
78
79
**Usage Example:**
80
81
```python
82
from python_multipart import Field
83
84
# Create field directly
85
field = Field(b'username')
86
field.write(b'john_doe')
87
field.finalize()
88
89
print(f"Field: {field.field_name.decode('utf-8')} = {field.value.decode('utf-8')}")
90
91
# Create field from value
92
email_field = Field.from_value(b'email', b'john@example.com')
93
print(f"Email: {email_field.value.decode('utf-8')}")
94
95
# Handle empty field
96
empty_field = Field(b'optional_field')
97
empty_field.set_none()
98
print(f"Empty field value: {empty_field.value}") # None
99
```
100
101
### File Class
102
103
Handles writing file data to memory or disk with configurable thresholds and automatic spillover for large files.
104
105
```python { .api }
106
class File:
107
"""
108
Handles file uploads with configurable memory/disk storage.
109
"""
110
111
def __init__(
112
self,
113
file_name: bytes | None,
114
field_name: bytes | None = None,
115
config: FileConfig = {}
116
):
117
"""
118
Initialize File object.
119
120
Parameters:
121
- file_name: Original filename as bytes
122
- field_name: Form field name as bytes
123
- config: Configuration dict for file handling
124
"""
125
126
def flush_to_disk(self) -> None:
127
"""If the file is already on-disk, do nothing. Otherwise, copy from
128
the in-memory buffer to a disk file, and then reassign our internal
129
file object to this new disk file.
130
131
Note that if you attempt to flush a file that is already on-disk, a
132
warning will be logged to this module's logger."""
133
134
def _get_disk_file(self) -> BufferedRandom:
135
"""This function is responsible for getting a file object on-disk for us.
136
137
Creates either a named temporary file or uses configured upload directory
138
based on configuration options.
139
140
Returns:
141
BufferedRandom file object opened for writing
142
143
Raises:
144
FileError: If unable to create or open disk file
145
"""
146
147
def write(self, data: bytes) -> int:
148
"""
149
Write data to file.
150
151
Parameters:
152
- data: Bytes to write
153
154
Returns:
155
Number of bytes written
156
"""
157
158
def on_data(self, data: bytes) -> int:
159
"""This method is a callback that will be called whenever data is
160
written to the File.
161
162
Parameters:
163
- data: The data to write to the file
164
165
Returns:
166
The number of bytes written"""
167
168
def on_end(self) -> None:
169
"""This method is called whenever the File is finalized."""
170
171
def finalize(self) -> None:
172
"""Finalize the form file. This will not close the underlying file,
173
but simply signal that we are finished writing to the File."""
174
175
def close(self) -> None:
176
"""Close the File object. This will actually close the underlying
177
file object (whether it's a io.BytesIO or an actual file object)."""
178
179
def __repr__(self) -> str:
180
"""Return string representation: File(file_name=..., field_name=...)."""
181
182
# Properties
183
@property
184
def field_name(self) -> bytes | None:
185
"""The form field associated with this file. May be None if there isn't
186
one, for example when we have an application/octet-stream upload."""
187
188
@property
189
def file_name(self) -> bytes | None:
190
"""The file name given in the upload request."""
191
192
@property
193
def actual_file_name(self) -> bytes | None:
194
"""The file name that this file is saved as. Will be None if it's not
195
currently saved on disk."""
196
197
@property
198
def file_object(self) -> BytesIO | BufferedRandom:
199
"""The file object that we're currently writing to. Note that this
200
will either be an instance of a io.BytesIO, or a regular file object."""
201
202
@property
203
def size(self) -> int:
204
"""The total size of this file, counted as the number of bytes that
205
currently have been written to the file."""
206
207
@property
208
def in_memory(self) -> bool:
209
"""A boolean representing whether or not this file object is currently
210
stored in-memory or on-disk."""
211
```
212
213
**Configuration Options:**
214
215
```python { .api }
216
FileConfig = {
217
'UPLOAD_DIR': str | bytes | None, # Directory for file uploads
218
'UPLOAD_DELETE_TMP': bool, # Delete temp files automatically
219
'UPLOAD_KEEP_FILENAME': bool, # Keep original filename
220
'UPLOAD_KEEP_EXTENSIONS': bool, # Keep file extensions
221
'MAX_MEMORY_FILE_SIZE': int # Max size before writing to disk
222
}
223
```
224
225
**Usage Example:**
226
227
```python
228
from python_multipart import File
229
import os
230
import tempfile
231
232
# Configure file handling
233
config = {
234
'UPLOAD_DIR': '/tmp/uploads',
235
'UPLOAD_KEEP_FILENAME': True,
236
'UPLOAD_KEEP_EXTENSIONS': True,
237
'MAX_MEMORY_FILE_SIZE': 1024 * 1024, # 1MB
238
'UPLOAD_DELETE_TMP': False
239
}
240
241
# Create upload directory if it doesn't exist
242
os.makedirs(config['UPLOAD_DIR'], exist_ok=True)
243
244
# Create file object
245
uploaded_file = File(
246
file_name=b'document.pdf',
247
field_name=b'file_upload',
248
config=config
249
)
250
251
# Simulate writing file content
252
file_content = b'PDF content would go here...' * 1000
253
uploaded_file.write(file_content)
254
uploaded_file.finalize()
255
256
print(f"File: {uploaded_file.file_name.decode('utf-8')}")
257
print(f"Field: {uploaded_file.field_name.decode('utf-8')}")
258
print(f"Size: {uploaded_file.size} bytes")
259
print(f"In memory: {uploaded_file.in_memory}")
260
261
if not uploaded_file.in_memory:
262
print(f"Saved to: {uploaded_file.actual_file_name.decode('utf-8')}")
263
264
# Always close when done
265
uploaded_file.close()
266
```
267
268
**Memory Management Example:**
269
270
```python
271
from python_multipart import File
272
from io import BytesIO
273
274
def handle_file_upload(file_data, filename, field_name):
275
"""Demonstrate automatic memory-to-disk spillover."""
276
277
# Small files stay in memory
278
small_config = {'MAX_MEMORY_FILE_SIZE': 10 * 1024 * 1024} # 10MB
279
small_file = File(filename.encode(), field_name.encode(), small_config)
280
281
# Large files go to disk
282
large_config = {'MAX_MEMORY_FILE_SIZE': 1024} # 1KB
283
large_file = File(filename.encode(), field_name.encode(), large_config)
284
285
# Write same data to both
286
for file_obj in [small_file, large_file]:
287
file_obj.write(file_data)
288
file_obj.finalize()
289
290
print(f"File size: {file_obj.size}")
291
print(f"In memory: {file_obj.in_memory}")
292
293
if file_obj.in_memory:
294
# Access data directly from memory
295
data = file_obj.file_object.getvalue()
296
print(f"Memory data length: {len(data)}")
297
else:
298
# File is on disk
299
print(f"Disk file: {file_obj.actual_file_name}")
300
301
# Read from disk file
302
with open(file_obj.actual_file_name, 'rb') as f:
303
disk_data = f.read()
304
print(f"Disk data length: {len(disk_data)}")
305
306
file_obj.close()
307
308
# Test with sample data
309
test_data = b'x' * 2048 # 2KB of data
310
handle_file_upload(test_data, 'test.txt', 'upload')
311
```
312
313
**File Lifecycle Management:**
314
315
```python
316
from python_multipart import File
317
import tempfile
318
import os
319
320
def process_upload_with_cleanup(file_content, filename):
321
"""Demonstrate proper file lifecycle management."""
322
323
config = {
324
'UPLOAD_DIR': tempfile.gettempdir(),
325
'UPLOAD_DELETE_TMP': True, # Auto-delete temp files
326
'MAX_MEMORY_FILE_SIZE': 1024
327
}
328
329
file_obj = File(filename.encode(), b'upload', config)
330
331
try:
332
# Write file content
333
file_obj.write(file_content)
334
file_obj.finalize()
335
336
# Process the file
337
if file_obj.in_memory:
338
# Work with in-memory file
339
content = file_obj.file_object.getvalue()
340
return f"Processed {len(content)} bytes in memory"
341
else:
342
# Work with disk file
343
temp_path = file_obj.actual_file_name.decode()
344
345
# Process file on disk
346
with open(temp_path, 'rb') as f:
347
content = f.read()
348
349
return f"Processed {len(content)} bytes from disk"
350
351
finally:
352
# Always clean up
353
file_obj.close() # This will delete temp files if configured
354
355
# Usage
356
result = process_upload_with_cleanup(b'Large file content...', 'data.bin')
357
print(result)
358
```
359
360
### Integration with Parsers
361
362
Field and File objects are automatically created by FormParser and passed to callbacks:
363
364
```python
365
from python_multipart import FormParser
366
367
def handle_form_data(content_type, input_stream):
368
processed_fields = []
369
processed_files = []
370
371
def on_field(field):
372
# Field object is automatically created and populated
373
processed_fields.append({
374
'name': field.field_name.decode('utf-8'),
375
'value': field.value.decode('utf-8') if field.value else None
376
})
377
field.close() # Clean up
378
379
def on_file(file):
380
# File object is automatically created and populated
381
file_info = {
382
'field_name': file.field_name.decode('utf-8'),
383
'filename': file.file_name.decode('utf-8') if file.file_name else None,
384
'size': file.size,
385
'in_memory': file.in_memory
386
}
387
388
if file.in_memory:
389
# Small file - data is in memory
390
file_info['content'] = file.file_object.getvalue()
391
else:
392
# Large file - saved to disk
393
file_info['temp_path'] = file.actual_file_name.decode('utf-8')
394
395
processed_files.append(file_info)
396
file.close() # Important: clean up temp files
397
398
# Parser automatically creates Field/File objects
399
parser = FormParser(content_type, on_field, on_file)
400
401
# Process input stream
402
while True:
403
chunk = input_stream.read(8192)
404
if not chunk:
405
break
406
parser.write(chunk)
407
408
parser.finalize()
409
parser.close()
410
411
return {
412
'fields': processed_fields,
413
'files': processed_files
414
}
415
```