Ticket #7614: 7814_cleanups.diff

File 7814_cleanups.diff, 25.5 KB (added by Michael Axiak, 16 years ago)

Update to the patch...thanks vomjom!

  • django/db/models/base.py

     
    536536            # This is a normal uploadedfile that we can stream.
    537537            fp = open(full_filename, 'wb')
    538538            locks.lock(fp, locks.LOCK_EX)
    539             for chunk in raw_field.chunk():
     539            for chunk in raw_field.chunks():
    540540                fp.write(chunk)
    541541            locks.unlock(fp)
    542542            fp.close()
  • django/db/models/fields/__init__.py

     
    766766    def get_db_prep_save(self, value):
    767767        "Returns field's value prepared for saving into a database."
    768768        # Need to convert UploadedFile objects provided via a form to unicode for database insertion
    769         if value is None:
     769        if hasattr(value, 'name'):
     770            return value.name
     771        elif value is None:
    770772            return None
    771         return unicode(value)
     773        else:
     774            return unicode(value)
    772775
    773776    def get_manipulator_fields(self, opts, manipulator, change, name_prefix='', rel=False, follow=True):
    774777        field_list = Field.get_manipulator_fields(self, opts, manipulator, change, name_prefix, rel, follow)
     
    842845            # We don't need to raise a warning because Model._save_FIELD_file will
    843846            # do so for us.
    844847            try:
    845                 file_name = file.file_name
     848                file_name = file.name
    846849            except AttributeError:
    847850                file_name = file['filename']
    848851
     
    857860        return os.path.normpath(f)
    858861
    859862    def save_form_data(self, instance, data):
    860         from django.newforms.fields import UploadedFile
     863        from django.core.files.uploadedfile import UploadedFile
    861864        if data and isinstance(data, UploadedFile):
    862             getattr(instance, "save_%s_file" % self.name)(data.filename, data.data, save=False)
     865            getattr(instance, "save_%s_file" % self.name)(data.name, data, save=False)
    863866
    864867    def formfield(self, **kwargs):
    865868        defaults = {'form_class': forms.FileField}
  • django/oldforms/__init__.py

     
    686686            if upload_errors:
    687687                raise validators.CriticalValidationError, upload_errors
    688688        try:
    689             file_size = new_data.file_size
     689            file_size = new_data.size
    690690        except AttributeError:
    691691            file_size = len(new_data['content'])
    692692        if not file_size:
  • django/core/files/uploadedfile.py

     
    33"""
    44
    55import os
     6import tempfile
     7import warnings
    68try:
    79    from cStringIO import StringIO
    810except ImportError:
    911    from StringIO import StringIO
    1012
    11 __all__ = ('UploadedFile', 'TemporaryUploadedFile', 'InMemoryUploadedFile')
     13from django.conf import settings
    1214
     15
     16__all__ = ('UploadedFile', 'TemporaryUploadedFile', 'InMemoryUploadedFile', 'SimpleUploadedFile')
     17
    1318class UploadedFile(object):
    1419    """
    1520    A abstract uploaded file (``TemporaryUploadedFile`` and
     
    2025    """
    2126    DEFAULT_CHUNK_SIZE = 64 * 2**10
    2227
    23     def __init__(self, file_name=None, content_type=None, file_size=None, charset=None):
    24         self.file_name = file_name
    25         self.file_size = file_size
     28    def __init__(self, name=None, content_type=None, size=None, charset=None):
     29        self.name = name
     30        self.size = size
    2631        self.content_type = content_type
    2732        self.charset = charset
    2833
    2934    def __repr__(self):
    30         return "<%s: %s (%s)>" % (self.__class__.__name__, self.file_name, self.content_type)
     35        return "<%s: %s (%s)>" % (self.__class__.__name__, self.name, self.content_type)
    3136
    32     def _set_file_name(self, name):
     37    def _get_name(self):
     38        return self._name
     39
     40    def _set_name(self, name):
    3341        # Sanitize the file name so that it can't be dangerous.
    3442        if name is not None:
    3543            # Just use the basename of the file -- anything else is dangerous.
    3644            name = os.path.basename(name)
    37            
     45
    3846            # File names longer than 255 characters can cause problems on older OSes.
    3947            if len(name) > 255:
    4048                name, ext = os.path.splitext(name)
    4149                name = name[:255 - len(ext)] + ext
    42                
    43         self._file_name = name
    44        
     50
     51        self._name = name
     52
     53    name = property(_get_name, _set_name)
     54
     55
    4556    def _get_file_name(self):
    46         return self._file_name
    47        
    48     file_name = property(_get_file_name, _set_file_name)
     57        warnings.warn(
     58            message = "To access the file name, please use the .name attribute.",
     59            category = DeprecationWarning,
     60            stacklevel = 2
     61            )
     62        return self.name
    4963
    50     def chunk(self, chunk_size=None):
     64    def _set_file_name(self, name):
     65        warnings.warn(
     66            message = "To access the file name, please use the .name attribute.",
     67            category = DeprecationWarning,
     68            stacklevel = 2
     69            )
     70        self.name = name
     71
     72    file_name = filename = property(_get_file_name, _set_file_name)
     73
     74
     75    def _get_file_size(self):
     76        warnings.warn(
     77            message = "To access the file size, please use the .size attribute.",
     78            category = DeprecationWarning,
     79            stacklevel = 2
     80            )
     81        return self.size
     82
     83    def _set_file_size(self, size):
     84        warnings.warn(
     85            message = "To access the file size, please use the .size attribute.",
     86            category = DeprecationWarning,
     87            stacklevel = 2
     88            )
     89        self.size = size
     90
     91    file_size = property(_get_file_size, _set_file_size)
     92
     93
     94    def _get_data(self):
     95        warnings.warn(
     96            message = "To access the data, please use the new UploadedFile API.",
     97            category = DeprecationWarning,
     98            stacklevel = 2
     99            )
     100        data = self.read()
     101        if hasattr(self, 'seek'):
     102            self.seek(0)
     103        return data
     104
     105    data = property(_get_data)
     106
     107
     108    def chunks(self, chunk_size=None):
    51109        """
    52110        Read the file and yield chucks of ``chunk_size`` bytes (defaults to
    53111        ``UploadedFile.DEFAULT_CHUNK_SIZE``).
     
    58116        if hasattr(self, 'seek'):
    59117            self.seek(0)
    60118        # Assume the pointer is at zero...
    61         counter = self.file_size
     119        counter = self.size
    62120
    63121        while counter > 0:
    64122            yield self.read(chunk_size)
    65123            counter -= chunk_size
    66124
     125    def chunk(self, *args, **kwargs):
     126        warnings.warn(
     127            message = "Please use .chunks() instead of .chunk().",
     128            category = DeprecationWarning,
     129            stacklevel = 2
     130            )
     131        for chunk in self.chunks():
     132            yield chunk
     133
    67134    def multiple_chunks(self, chunk_size=None):
    68135        """
    69136        Returns ``True`` if you can expect multiple chunks.
     
    74141        """
    75142        if not chunk_size:
    76143            chunk_size = UploadedFile.DEFAULT_CHUNK_SIZE
    77         return self.file_size < chunk_size
     144        return self.size > chunk_size
    78145
    79     # Abstract methods; subclasses *must* default read() and probably should
     146    # Abstract methods; subclasses *must* define read() and probably should
    80147    # define open/close.
    81148    def read(self, num_bytes=None):
    82149        raise NotImplementedError()
     
    87154    def close(self):
    88155        pass
    89156
     157    def xreadlines(self):
     158        return self
     159
     160    def readlines(self):
     161        return list(self.xreadlines())
     162
     163    def __iter__(self):
     164        # Iterate over this file-like object by newlines
     165        buffer_ = None
     166        for chunk in self.chunks():
     167            chunk_buffer = StringIO(chunk)
     168
     169            for line in chunk_buffer:
     170                if buffer_:
     171                    line = buffer_ + line
     172                    buffer_ = None
     173
     174                # If this is the end of a line, yield
     175                # otherwise, wait for the next round
     176                if line[-1] in ('\n', '\r'):
     177                    yield line
     178                else:
     179                    buffer_ = line
     180
     181        if buffer_ is not None:
     182            yield buffer_
     183
    90184    # Backwards-compatible support for uploaded-files-as-dictionaries.
    91185    def __getitem__(self, key):
    92         import warnings
    93186        warnings.warn(
    94187            message = "The dictionary access of uploaded file objects is deprecated. Use the new object interface instead.",
    95188            category = DeprecationWarning,
    96189            stacklevel = 2
    97190        )
    98191        backwards_translate = {
    99             'filename': 'file_name',
     192            'filename': 'name',
    100193            'content-type': 'content_type',
    101194            }
    102195
    103196        if key == 'content':
    104197            return self.read()
    105198        elif key == 'filename':
    106             return self.file_name
     199            return self.name
    107200        elif key == 'content-type':
    108201            return self.content_type
    109202        else:
    110203            return getattr(self, key)
    111204
     205
     206def _proxy_to_file(method_name):
     207    # Function factory to build proxy methods for the _file object.
     208    def __inner_function(self, *args, **kwargs):
     209        return getattr(self._file, method_name)(*args, **kwargs)
     210    __inner_function.__name__ = method_name
     211    return __inner_function
     212
     213
    112214class TemporaryUploadedFile(UploadedFile):
    113215    """
    114216    A file uploaded to a temporary location (i.e. stream-to-disk).
    115217    """
     218    def __init__(self, name, content_type, size, charset):
     219        super(TemporaryUploadedFile, self).__init__(name, content_type, size, charset)
     220        if settings.FILE_UPLOAD_TEMP_DIR:
     221            self._file = tempfile.NamedTemporaryFile(suffix='.upload', dir=settings.FILE_UPLOAD_TEMP_DIR)
     222        else:
     223            self._file = tempfile.NamedTemporaryFile(suffix='.upload')
    116224
    117     def __init__(self, file, file_name, content_type, file_size, charset):
    118         super(TemporaryUploadedFile, self).__init__(file_name, content_type, file_size, charset)
    119         self.file = file
    120         self.path = file.name
    121         self.file.seek(0)
    122 
    123225    def temporary_file_path(self):
    124226        """
    125227        Returns the full path of this file.
    126228        """
    127         return self.path
     229        return self.name
    128230
    129     def read(self, *args, **kwargs):
    130         return self.file.read(*args, **kwargs)
     231    # Functions from the local file object
     232    read = _proxy_to_file('read')
     233    seek = _proxy_to_file('seek')
     234    write = _proxy_to_file('write')
     235    close = _proxy_to_file('close')
     236    __iter__ = _proxy_to_file('__iter__')
     237    readlines = _proxy_to_file('readlines')
     238    xreadlines = _proxy_to_file('xreadlines')
    131239
    132     def open(self):
    133         self.seek(0)
    134240
    135     def seek(self, *args, **kwargs):
    136         self.file.seek(*args, **kwargs)
    137 
    138241class InMemoryUploadedFile(UploadedFile):
    139242    """
    140243    A file uploaded into memory (i.e. stream-to-memory).
    141244    """
    142     def __init__(self, file, field_name, file_name, content_type, file_size, charset):
    143         super(InMemoryUploadedFile, self).__init__(file_name, content_type, file_size, charset)
     245    def __init__(self, file, field_name, name, content_type, size, charset):
     246        super(InMemoryUploadedFile, self).__init__(name, content_type, size, charset)
    144247        self.file = file
    145248        self.field_name = field_name
    146249        self.file.seek(0)
     
    154257    def read(self, *args, **kwargs):
    155258        return self.file.read(*args, **kwargs)
    156259
    157     def chunk(self, chunk_size=None):
     260    def chunks(self, chunk_size=None):
    158261        self.file.seek(0)
    159262        yield self.read()
    160263
     
    168271    """
    169272    def __init__(self, name, content, content_type='text/plain'):
    170273        self.file = StringIO(content or '')
    171         self.file_name = name
     274        self.name = name
    172275        self.field_name = None
    173         self.file_size = len(content or '')
     276        self.size = len(content or '')
    174277        self.content_type = content_type
    175278        self.charset = None
    176279        self.file.seek(0)
  • django/core/files/uploadhandler.py

     
    132132        Create the file object to append to as data is coming in.
    133133        """
    134134        super(TemporaryFileUploadHandler, self).new_file(file_name, *args, **kwargs)
    135         self.file = TemporaryFile(settings.FILE_UPLOAD_TEMP_DIR)
    136         self.write = self.file.write
     135        self.file = TemporaryUploadedFile(self.file_name, self.content_type, 0, self.charset)
    137136
    138137    def receive_data_chunk(self, raw_data, start):
    139         self.write(raw_data)
     138        self.file.write(raw_data)
    140139
    141140    def file_complete(self, file_size):
    142141        self.file.seek(0)
    143         return TemporaryUploadedFile(
    144             file = self.file,
    145             file_name = self.file_name,
    146             content_type = self.content_type,
    147             file_size = file_size,
    148             charset = self.charset
    149         )
     142        self.file.size = file_size
     143        return self.file
    150144
    151145class MemoryFileUploadHandler(FileUploadHandler):
    152146    """
     
    189183        return InMemoryUploadedFile(
    190184            file = self.file,
    191185            field_name = self.field_name,
    192             file_name = self.file_name,
     186            name = self.file_name,
    193187            content_type = self.content_type,
    194             file_size = file_size,
     188            size = file_size,
    195189            charset = self.charset
    196190        )
    197191
    198 class TemporaryFile(object):
    199     """
    200     A temporary file that tries to delete itself when garbage collected.
    201     """
    202     def __init__(self, dir):
    203         if not dir:
    204             dir = tempfile.gettempdir()
    205         try:
    206             (fd, name) = tempfile.mkstemp(suffix='.upload', dir=dir)
    207             self.file = os.fdopen(fd, 'w+b')
    208         except (OSError, IOError):
    209             raise OSError("Could not create temporary file for uploading, have you set settings.FILE_UPLOAD_TEMP_DIR correctly?")
    210         self.name = name
    211192
    212     def __getattr__(self, name):
    213         a = getattr(self.__dict__['file'], name)
    214         if type(a) != type(0):
    215             setattr(self, name, a)
    216         return a
    217 
    218     def __del__(self):
    219         try:
    220             os.unlink(self.name)
    221         except OSError:
    222             pass
    223 
    224193def load_handler(path, *args, **kwargs):
    225194    """
    226195    Given a path to a handler, return an instance of that handler.
  • django/newforms/fields.py

     
    2727
    2828from util import ErrorList, ValidationError
    2929from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, FileInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple, DateTimeInput
     30from django.core.files.uploadedfile import SimpleUploadedFile as UploadedFile
    3031
    31 
    3232__all__ = (
    3333    'Field', 'CharField', 'IntegerField',
    3434    'DEFAULT_DATE_INPUT_FORMATS', 'DateField',
     
    419419    # It's OK if Django settings aren't configured.
    420420    URL_VALIDATOR_USER_AGENT = 'Django (http://www.djangoproject.com/)'
    421421
    422 class UploadedFile(StrAndUnicode):
    423     "A wrapper for files uploaded in a FileField"
    424     def __init__(self, filename, data):
    425         self.filename = filename
    426         self.data = data
    427422
    428     def __unicode__(self):
    429         """
    430         The unicode representation is the filename, so that the pre-database-insertion
    431         logic can use UploadedFile objects
    432         """
    433         return self.filename
    434 
    435423class FileField(Field):
    436424    widget = FileInput
    437425    default_error_messages = {
     
    460448                category = DeprecationWarning,
    461449                stacklevel = 2
    462450            )
     451            data = UploadedFile(data['filename'], data['content'])
    463452
    464453        try:
    465             file_name = data.file_name
    466             file_size = data.file_size
     454            file_name = data.name
     455            file_size = data.size
    467456        except AttributeError:
    468             try:
    469                 file_name = data.get('filename')
    470                 file_size = bool(data['content'])
    471             except (AttributeError, KeyError):
    472                 raise ValidationError(self.error_messages['invalid'])
     457            raise ValidationError(self.error_messages['invalid'])
    473458
    474459        if not file_name:
    475460            raise ValidationError(self.error_messages['invalid'])
    476461        if not file_size:
    477462            raise ValidationError(self.error_messages['empty'])
    478463
    479         return UploadedFile(file_name, data)
     464        return data
    480465
    481466class ImageField(FileField):
    482467    default_error_messages = {
  • tests/modeltests/model_forms/models.py

     
    803803>>> f.is_valid()
    804804True
    805805>>> type(f.cleaned_data['file'])
    806 <class 'django.newforms.fields.UploadedFile'>
     806<class 'django.core.files.uploadedfile.SimpleUploadedFile'>
    807807>>> instance = f.save()
    808808>>> instance.file
    809809u'...test1.txt'
     
    814814>>> f.is_valid()
    815815True
    816816>>> type(f.cleaned_data['file'])
    817 <class 'django.newforms.fields.UploadedFile'>
     817<class 'django.core.files.uploadedfile.SimpleUploadedFile'>
    818818>>> instance = f.save()
    819819>>> instance.file
    820820u'...test1.txt'
     
    906906>>> f.is_valid()
    907907True
    908908>>> type(f.cleaned_data['image'])
    909 <class 'django.newforms.fields.UploadedFile'>
     909<class 'django.core.files.uploadedfile.SimpleUploadedFile'>
    910910>>> instance = f.save()
    911911>>> instance.image
    912912u'...test.png'
     
    918918>>> f.is_valid()
    919919True
    920920>>> type(f.cleaned_data['image'])
    921 <class 'django.newforms.fields.UploadedFile'>
     921<class 'django.core.files.uploadedfile.SimpleUploadedFile'>
    922922>>> instance = f.save()
    923923>>> instance.image
    924924u'...test.png'
  • tests/regressiontests/forms/fields.py

     
    800800ValidationError: [u'The submitted file is empty.']
    801801
    802802>>> type(f.clean(SimpleUploadedFile('name', 'Some File Content')))
    803 <class 'django.newforms.fields.UploadedFile'>
     803<class 'django.core.files.uploadedfile.SimpleUploadedFile'>
    804804
    805805>>> type(f.clean(SimpleUploadedFile('name', 'Some File Content'), 'files/test4.pdf'))
    806 <class 'django.newforms.fields.UploadedFile'>
     806<class 'django.core.files.uploadedfile.SimpleUploadedFile'>
    807807
    808808# URLField ##################################################################
    809809
  • docs/upload_handling.txt

     
    2222    class UploadFileForm(forms.Form):
    2323        title = forms.CharField(max_length=50)
    2424        file  = forms.FileField()
    25        
     25
    2626A view handling this form will receive the file data in ``request.FILES``, which
    2727is a dictionary containing a key for each ``FileField`` (or ``ImageField``, or
    2828other ``FileField`` subclass) in the form. So the data from the above form would
     
    6464    ``UploadedFile.read()``
    6565        Read the entire uploaded data from the file. Be careful with this
    6666        method: if the uploaded file is huge it can overwhelm your system if you
    67         try to read it into memory. You'll probably want to use ``chunk()``
     67        try to read it into memory. You'll probably want to use ``chunks()``
    6868        instead; see below.
    69        
     69
    7070    ``UploadedFile.multiple_chunks()``
    7171        Returns ``True`` if the uploaded file is big enough to require
    7272        reading in multiple chunks. By default this will be any file
    7373        larger than 2.5 megabytes, but that's configurable; see below.
    74    
     74
    7575    ``UploadedFile.chunk()``
    7676        A generator returning chunks of the file. If ``multiple_chunks()`` is
    7777        ``True``, you should use this method in a loop instead of ``read()``.
    78        
     78
    7979        In practice, it's often easiest simply to use ``chunks()`` all the time;
    8080        see the example below.
    81    
     81
    8282    ``UploadedFile.file_name``
    8383        The name of the uploaded file (e.g. ``my_file.txt``).
    84        
     84
    8585    ``UploadedFile.file_size``
    8686        The size, in bytes, of the uploaded file.
    87        
     87
    8888There are a few other methods and attributes available on ``UploadedFile``
    8989objects; see `UploadedFile objects`_ for a complete reference.
    9090
    9191Putting it all together, here's a common way you might handle an uploaded file::
    92    
     92
    9393    def handle_uploaded_file(f):
    9494        destination = open('some/file/name.txt', 'wb')
    9595        for chunk in f.chunks():
     
    126126        The maximum size, in bytes, for files that will be uploaded
    127127        into memory. Files larger than ``FILE_UPLOAD_MAX_MEMORY_SIZE``
    128128        will be streamed to disk.
    129        
     129
    130130        Defaults to 2.5 megabytes.
    131        
     131
    132132    ``FILE_UPLOAD_TEMP_DIR``
    133133        The directory where uploaded files larger than ``FILE_UPLOAD_TEMP_DIR``
    134134        will be stored.
    135        
     135
    136136        Defaults to your system's standard temporary directory (i.e. ``/tmp`` on
    137137        most Unix-like systems).
    138        
     138
    139139    ``FILE_UPLOAD_HANDLERS``
    140140        The actual handlers for uploaded files. Changing this setting
    141141        allows complete customization -- even replacement -- of
    142142        Django's upload process. See `upload handlers`_, below,
    143143        for details.
    144        
     144
    145145        Defaults to::
    146        
     146
    147147            ("django.core.files.uploadhandler.MemoryFileUploadHandler",
    148148             "django.core.files.uploadhandler.TemporaryFileUploadHandler",)
    149            
     149
    150150        Which means "try to upload to memory first, then fall back to temporary
    151151        files."
    152152
     
    161161        Returns a byte string of length ``num_bytes``, or the complete file if
    162162        ``num_bytes`` is ``None``.
    163163
    164     ``UploadedFile.chunk(self, chunk_size=None)``
     164    ``UploadedFile.chunks(self, chunk_size=None)``
    165165        A generator yielding small chunks from the file. If ``chunk_size`` isn't
    166         given, chunks will be 64 kb.
     166        given, chunks will be 64 KB.
    167167
    168168    ``UploadedFile.multiple_chunks(self, chunk_size=None)``
    169169        Returns ``True`` if you can expect more than one chunk when calling
    170         ``UploadedFile.chunk(self, chunk_size)``.
     170        ``UploadedFile.chunks(self, chunk_size)``.
    171171
    172172    ``UploadedFile.file_size``
    173173        The size, in bytes, of the uploaded file.
    174    
     174
    175175    ``UploadedFile.file_name``
    176176        The name of the uploaded file as provided by the user.
    177    
     177
    178178    ``UploadedFile.content_type``
    179179        The content-type header uploaded with the file (e.g. ``text/plain`` or
    180180        ``application/pdf``). Like any data supplied by the user, you shouldn't
    181181        trust that the uploaded file is actually this type. You'll still need to
    182182        validate that the file contains the content that the content-type header
    183183        claims -- "trust but verify."
    184    
     184
    185185    ``UploadedFile.charset``
    186186        For ``text/*`` content-types, the character set (i.e. ``utf8``) supplied
    187187        by the browser. Again, "trust but verify" is the best policy here.
    188188
     189    ``UploadedFile.__iter__()``
     190        Iterates over the lines in the file.
     191
    189192    ``UploadedFile.temporary_file_path()``
    190193        Only files uploaded onto disk will have this method; it returns the full
    191194        path to the temporary uploaded file.
    192195
     196
    193197Upload Handlers
    194198===============
    195199
  • docs/newforms.txt

     
    13311331    * Validates that non-empty file data has been bound to the form.
    13321332    * Error message keys: ``required``, ``invalid``, ``missing``, ``empty``
    13331333
    1334 An ``UploadedFile`` object has two attributes:
     1334To learn more about the ``UploadedFile`` object, see the `file uploads documentation`_.
    13351335
    1336     ======================  ====================================================
    1337     Attribute               Description
    1338     ======================  ====================================================
    1339     ``filename``            The name of the file, provided by the uploading
    1340                             client.
    1341                            
    1342     ``content``             The array of bytes comprising the file content.
    1343     ======================  ====================================================
    1344 
    1345 The string representation of an ``UploadedFile`` is the same as the filename
    1346 attribute.
    1347 
    13481336When you use a ``FileField`` in a form, you must also remember to
    13491337`bind the file data to the form`_.
    13501338
     1339.. _file uploads documentation: ../upload_handling/
    13511340.. _`bind the file data to the form`: `Binding uploaded files to a form`_
    13521341
    13531342``FilePathField``
  • docs/settings.txt

     
    279279
    280280The database backend to use. The build-in database backends are
    281281``'postgresql_psycopg2'``, ``'postgresql'``, ``'mysql'``, ``'mysql_old'``,
    282 ``'sqlite3'``, ``'oracle'``, and ``'oracle'``.
     282``'sqlite3'``, and ``'oracle'``.
    283283
    284284In the Django development version, you can use a database backend that doesn't
    285285ship with Django by setting ``DATABASE_ENGINE`` to a fully-qualified path (i.e.
Back to Top