Repository: joke2k/faker

Issue:
Feature request: file_path extension=False to generate a path without extension
```
faker.file_path(extension=None)
```
 will generate something like `/event/amount.js`

### Actual behavior

There is no way to generate a path without an extension. 

### Expected behavior

This is counter-intuitive - `exrtension? None` should mean that there is no extension. 
And it would be useful to have a way to generate a path without an extension, regardless of how it is done.


### Suggestion

In order not to break backward compatibility, can I suggest changing extension to be either None, bool or a string, with the following behaviour

* True -> same as None
* False ->  no extension
* None ->  as it is now 
* a string -> the string

Thanks



Discussion:
Happy to submit a PR if you like the idea
Hi @gotofritz thank you for your proposal.

I was wondering if it wouldn't be more congenial to pass a list of extensions from which the generator can draw an extension, overriding the current behaviour (which extracts from a static list). 
So, we would eventually have, 
None -> current behavior (default)
list -> a new list to draw extensions from, which in your case could potentially be empty.
Yeah that would also work. 
> Hi @gotofritz thank you for your proposal.
> 
> I was wondering if it wouldn't be more congenial to pass a list of extensions from which the generator can draw an extension, overriding the current behaviour (which extracts from a static list). So, we would eventually have, None -> current behavior (default) list -> a new list to draw extensions from, which in your case could potentially be empty.

Can we use some sort of a property to implement an dynamic extention generator?
If we do this we need to pass the list of extention we want each time we need to, but having a property like ``extentions``,
and changing it base on instances isn't a better idea?
In this way we just set our extentions in our class, and if we didn't the class uses the current behavior, in other word we use a defualt value for ``extentions`` and if we change the property we use that instead.
@parsariyahi I am not entirely sure what you are proposing, but it seems to go well beyond my simple request for 'ability to generate a path without extensions'. I suggest you make a separate proposal

Resolving patch:
diff --git a/faker/providers/file/__init__.py b/faker/providers/file/__init__.py
--- a/faker/providers/file/__init__.py
+++ b/faker/providers/file/__init__.py
@@ -1,7 +1,7 @@
 import string
 
 from collections import OrderedDict
-from typing import Dict, Optional
+from typing import Dict, Optional, Sequence, Union
 
 from .. import BaseProvider, ElementsType
 
@@ -223,21 +223,24 @@ def mime_type(self, category: Optional[str] = None) -> str:
     def file_name(self, category: Optional[str] = None, extension: Optional[str] = None) -> str:
         """Generate a random file name with extension.
 
-        If ``extension`` is ``None``, a random extension will be created under
-        the hood using |file_extension| with the specified ``category``. If a
-        value for ``extension`` is provided, the value will be used instead,
-        and ``category`` will be ignored. The actual name part itself is
-        generated using |word|.
+        If ``extension`` is ``None``, a random extension will be created
+        under the hood using |file_extension| with the specified
+        ``category``. If a value for ``extension`` is provided, the
+        value will be used instead, and ``category`` will be ignored.
+        The actual name part itself is generated using |word|. If
+        extension is an empty string then no extension will be added,
+        and file_name will be the same as |word|.
 
         :sample: size=10
         :sample: category='audio'
         :sample: extension='abcdef'
         :sample: category='audio', extension='abcdef'
+        :sample: extension=''
         """
         if extension is None:
             extension = self.file_extension(category)
         filename: str = self.generator.word()
-        return f"{filename}.{extension}"
+        return f"{filename}.{extension}" if extension else filename
 
     def file_extension(self, category: Optional[str] = None) -> str:
         """Generate a file extension under the specified ``category``.
@@ -257,23 +260,39 @@ def file_path(
         self,
         depth: int = 1,
         category: Optional[str] = None,
-        extension: Optional[str] = None,
+        extension: Optional[Union[str, Sequence[str]]] = None,
         absolute: Optional[bool] = True,
     ) -> str:
         """Generate an pathname to a file.
 
-        This method uses |file_name| under the hood to generate the file name
-        itself, and ``depth`` controls the depth of the directory path, and
-        |word| is used under the hood to generate the different directory names.
+        This method uses |file_name| under the hood to generate the file
+        name itself, and ``depth`` controls the depth of the directory
+        path, and |word| is used under the hood to generate the
+        different directory names.
 
-        If ``absolute`` is ``True`` (default), the generated path starts with
-        ``/`` and is absolute. Otherwise, the generated path is relative.
+        If ``absolute`` is ``True`` (default), the generated path starts
+        with ``/`` and is absolute. Otherwise, the generated path is
+        relative.
+
+        If used, ``extension`` can be either a string, forcing that
+        extension, a sequence of strings (one will be picked at random),
+        or an empty sequence (the path will have no extension). Default
+        behaviour is the same as |file_name|
 
         :sample: size=10
         :sample: depth=3
         :sample: depth=5, category='video'
         :sample: depth=5, category='video', extension='abcdef'
+        :sample: extension=[]
+        :sample: extension=''
+        :sample: extension=["a", "bc", "def"]
         """
+        if extension is not None and not isinstance(extension, str):
+            if len(extension):
+                extension = self.random_element(extension)
+            else:
+                extension = ""
+
         file: str = self.file_name(category, extension)
         path: str = f"/{file}"
         for _ in range(0, depth):


Test patch:
diff --git a/tests/providers/test_file.py b/tests/providers/test_file.py
--- a/tests/providers/test_file.py
+++ b/tests/providers/test_file.py
@@ -11,6 +11,21 @@ def setUp(self):
         self.fake = Faker()
         Faker.seed(0)
 
+    def test_file_name(self):
+        for _ in range(100):
+            file_name = self.fake.file_name()
+            assert re.search(r"\w+\.\w+", file_name)
+            file_name = self.fake.file_name(extension=None)
+            assert re.search(r"\w+\.\w+", file_name)
+            file_name = self.fake.file_name(extension="pdf")
+            assert re.search(r"\w+\.pdf$", file_name)
+            file_name = self.fake.file_name(category="image")
+            assert re.search(r"\w+\.(bmp|gif|jpeg|jpg|png|tiff)$", file_name)
+            file_name = self.fake.file_name(category="image", extension="abcdef")
+            assert re.search(r"\w+\.abcdef$", file_name)
+            file_name = self.fake.file_name(extension="")
+            assert re.search(r"\w+$", file_name)
+
     def test_file_path(self):
         for _ in range(100):
             file_path = self.fake.file_path()
@@ -20,9 +35,17 @@ def test_file_path(self):
             file_path = self.fake.file_path(depth=3)
             assert re.search(r"\/\w+\/\w+\/\w+\.\w+", file_path)
             file_path = self.fake.file_path(extension="pdf")
-            assert re.search(r"\/\w+\/\w+\.pdf", file_path)
+            assert re.search(r"\/\w+\/\w+\.pdf$", file_path)
+            file_path = self.fake.file_path(extension=["a", "bc", "def", "ghij", "klmno"])
+            assert re.search(r"\/\w+\/\w+\.(a|bc|def|ghij|klmno)$", file_path)
+            file_path = self.fake.file_path(extension=None)
+            assert re.search(r"\/\w+\/\w+\.\w+", file_path)
+            file_path = self.fake.file_path(extension="")
+            assert re.search(r"\/\w+\/\w+$", file_path)
+            file_path = self.fake.file_path(extension=[])
+            assert re.search(r"\/\w+\/\w+$", file_path)
             file_path = self.fake.file_path(category="image")
-            assert re.search(r"\/\w+\/\w+\.(bmp|gif|jpeg|jpg|png|tiff)", file_path)
+            assert re.search(r"\/\w+\/\w+\.(bmp|gif|jpeg|jpg|png|tiff)$", file_path)
 
     def test_unix_device(self):
         reg_device = re.compile(r"^/dev/(vd|sd|xvd)[a-z]$")