Repository: omaciel/fauxfactory

Issue:
[Proposal] some extra param to avoid string formatting
While using some string category method user like to matriculate string; they want string `start` with something. Its most common use-case; which, I observed while using `fauxfactory` in daily life. It will reduce lots of indirect string formatting.
```
def gen_alphanumeric(length=10, start=None):
    pass
```

small example:
```
In [1]: from fauxfactory import gen_alpha, gen_alphanumeric                                                                                       

In [2]: gen_alpha(start="volume")                                                                                                                 
Out[2]: 'volume_DGDvJNxZmO'

In [3]: gen_alpha(3, start="og")                                                                                                                  
Out[3]: 'og_OpD'

In [4]: gen_alphanumeric(start="vol")                                                                                                             
Out[4]: 'vol_DJBGf7Af2L'

In [5]: gen_alphanumeric(5, start="vol")                                                                                                          
Out[5]: 'vol_3pmw0'
```

This is basic example. we can go for more enhancement, we can even take care of `string length`.

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/omaciel/fauxfactory/pull/103?src=pr&el=h1) Report
> Merging [#103](https://codecov.io/gh/omaciel/fauxfactory/pull/103?src=pr&el=desc) into [master](https://codecov.io/gh/omaciel/fauxfactory/commit/df28434be0baed3e68e9c26986ff67f78b66d5b0?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/omaciel/fauxfactory/pull/103/graphs/tree.svg?width=650&token=jlm3hYbTbH&height=150&src=pr)](https://codecov.io/gh/omaciel/fauxfactory/pull/103?src=pr&el=tree)

```diff
@@          Coverage Diff          @@
##           master   #103   +/-   ##
=====================================
  Coverage     100%   100%           
=====================================
  Files           9      9           
  Lines         455    476   +21     
=====================================
+ Hits          455    476   +21
```


| [Impacted Files](https://codecov.io/gh/omaciel/fauxfactory/pull/103?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [fauxfactory/factories/strings.py](https://codecov.io/gh/omaciel/fauxfactory/pull/103/diff?src=pr&el=tree#diff-ZmF1eGZhY3RvcnkvZmFjdG9yaWVzL3N0cmluZ3MucHk=) | `100% <100%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/omaciel/fauxfactory/pull/103?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/omaciel/fauxfactory/pull/103?src=pr&el=footer). Last update [df28434...92d8a93](https://codecov.io/gh/omaciel/fauxfactory/pull/103?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

omaciel: Interesting... at first I interpreted the requirement as being `Given a start parameter, string generating methods should yield a string that starts with a string representation of said parameter, which should be counted toward the final length of the final string.`

For example:

```
In [1]: gen_alpha(start="volume") # defaults to 10 characters                                                                                                                 
Out[1]: 'volume_DGD' # 10 total chars
```

So is it standard to assume that given a method that offers both `length` and `start`, that the final string will be of length equals to the sum of `length + start` and an additional separator character? Should then `separator` be an optional parameter that defaults to `_`?

digitronik: > Interesting... at first I interpreted the requirement as being `Given a start parameter, string generating methods should yield a string that starts with a string representation of said parameter, which should be counted toward the final length of the final string.`

I agreed with this :+1: . We should to take care of final `random data` string `length`.
> 
> For example:
> 
> ```
> In [1]: gen_alpha(start="volume") # defaults to 10 characters                                                                                                                 
> Out[1]: 'volume_DGD' # 10 total chars
> ```
> So is it standard to assume that given a method that offers both `length` and `start`, that the final string will be of length equals to the sum of `length + start` and an additional separator character? Should then `separator` be an optional parameter that defaults to `_`?

@omaciel if you ack. As per me `separator` should optional `arg` and `default` value should `''`. It should user privilege that they want to add separator or not; even user can add separator as per gen `type`.
```
def gen_alpha(length=10, start=None, separator=''):
    pass
```

Patch:
diff --git a/fauxfactory/factories/strings.py b/fauxfactory/factories/strings.py
index 5e38b52..67f6e50 100644
--- a/fauxfactory/factories/strings.py
+++ b/fauxfactory/factories/strings.py
@@ -68,28 +68,35 @@ def gen_string(str_type, length=None, validator=None, default=None, tries=10):
 
 @check_len
 @check_validation
-def gen_alpha(length=10):
+def gen_alpha(length=10, start=None, separator=''):
     """Return a random string made up of alpha characters.
 
     :param int length: Length for random data.
+    :param str start: Random data start with.
+    :param char separator: Separator for start and random data.
     :returns: A random string made up of alpha characters.
     :rtype: str
 
     """
     random.seed()
     output_string = ''.join(
-        random.choice(string.ascii_letters) for i in range(length)
+        random.choice(string.ascii_letters) for _ in range(length)
     )
 
+    if start:
+        output_string = '{0}{1}{2}'.format(
+            start, separator, output_string)[0:length]
     return output_string
 
 
 @check_len
 @check_validation
-def gen_alphanumeric(length=10):
+def gen_alphanumeric(length=10, start=None, separator=''):
     """Return a random string made up of alpha and numeric characters.
 
     :param int length: Length for random data.
+    :param str start: Random data start with.
+    :param char separator: Separator for start and random data.
     :returns: A random string made up of alpha and numeric characters.
     :rtype: str
 
@@ -98,19 +105,24 @@ def gen_alphanumeric(length=10):
     output_string = ''.join(
         random.choice(
             string.ascii_letters + string.digits
-        ) for i in range(length))
+        ) for _ in range(length))
 
+    if start:
+        output_string = '{0}{1}{2}'.format(
+            start, separator, output_string)[0:length]
     return output_string
 
 
 @check_len
 @check_validation
-def gen_cjk(length=10):
+def gen_cjk(length=10, start=None, separator=''):
     """Return a random string made up of CJK characters.
 
     (Source: Wikipedia - CJK Unified Ideographs)
 
     :param int length: Length for random data.
+    :param str start: Random data start with.
+    :param char separator: Separator for start and random data.
     :returns: A random string made up of CJK characters.
     :rtype: str
 
@@ -120,15 +132,22 @@ def gen_cjk(length=10):
     # Generate codepoints, then convert the codepoints to a string. The
     # valid range of CJK codepoints is 0x4E00 - 0x9FCC, inclusive.
     codepoints = [random.randint(0x4E00, 0x9FCC) for _ in range(length)]
-    return ''.join(chr(codepoint) for codepoint in codepoints)
+    output_string = ''.join(chr(codepoint) for codepoint in codepoints)
+
+    if start:
+        output_string = '{0}{1}{2}'.format(
+            start, separator, output_string)[0:length]
+    return output_string
 
 
 @check_len
 @check_validation
-def gen_cyrillic(length=10):
+def gen_cyrillic(length=10, start=None, separator=''):
     """Return a random string made up of Cyrillic characters.
 
     :param int length: Length for random data.
+    :param str start: Random data start with.
+    :param char separator: Separator for start and random data.
     :returns: A random string made up of Cyrillic characters.
     :rtype: str
 
@@ -136,9 +155,14 @@ def gen_cyrillic(length=10):
     random.seed()
 
     # Generate codepoints, then convert the codepoints to a string. The
-    # valid range of Cyrillic codepoints is 0x410 - 0x4ff, inclusive.
+    # valid range of Cyrillic codepoints is 0x0400 - 0x04FF, inclusive.
     codepoints = [random.randint(0x0400, 0x04FF) for _ in range(length)]
-    return ''.join(chr(codepoint) for codepoint in codepoints)
+    output_string = ''.join(chr(codepoint) for codepoint in codepoints)
+
+    if start:
+        output_string = '{0}{1}{2}'.format(
+            start, separator, output_string)[0:length]
+    return output_string
 
 
 @check_len
@@ -236,12 +260,14 @@ def gen_iplum(words=None, paragraphs=None):
 
 @check_len
 @check_validation
-def gen_latin1(length=10):
+def gen_latin1(length=10, start=None, separator=''):
     """Return a random string made up of UTF-8 characters.
 
     (Font: Wikipedia - Latin-1 Supplement Unicode Block)
 
     :param int length: Length for random data.
+    :param str start: Random data start with.
+    :param char separator: Separator for start and random data.
     :returns: A random string made up of ``Latin1`` characters.
     :rtype: str
 
@@ -264,31 +290,45 @@ def gen_latin1(length=10):
         chr(random.choice(output_array)) for _ in range(length)
     )
 
+    if start:
+        output_string = '{0}{1}{2}'.format(
+            start, separator, output_string)[0:length]
     return output_string
 
 
 @check_len
 @check_validation
-def gen_numeric_string(length=10):
+def gen_numeric_string(length=10, start=None, separator=''):
     """Return a random string made up of numbers.
 
     :param int length: Length for random data.
+    :param str start: Random data start with.
+    :param char separator: Separator for start and random data.
     :returns: A random string made up of numbers.
     :rtype: str
 
     """
     random.seed()
-    return ''.join(random.choice(string.digits) for _ in range(length))
+    output_string = ''.join(
+        random.choice(string.digits) for _ in range(length)
+    )
+
+    if start:
+        output_string = '{0}{1}{2}'.format(
+            start, separator, output_string)[0:length]
+    return output_string
 
 
 @check_len
 @check_validation
-def gen_utf8(length=10, smp=True):
+def gen_utf8(length=10, smp=True, start=None, separator=''):
     """Return a random string made up of UTF-8 letters characters.
 
     Follows `RFC 3629`_.
 
     :param int length: Length for random data.
+    :param str start: Random data start with.
+    :param char separator: Separator for start and random data.
     :param bool smp: Include Supplementary Multilingual Plane (SMP)
         characters
     :returns: A random string made up of ``UTF-8`` letters characters.
@@ -299,17 +339,33 @@ def gen_utf8(length=10, smp=True):
     """
     UNICODE_LETTERS = [c for c in unicode_letters_generator(smp)]
     random.seed()
-    return ''.join([random.choice(UNICODE_LETTERS) for _ in range(length)])
+    output_string = ''.join(
+        [random.choice(UNICODE_LETTERS) for _ in range(length)]
+    )
+
+    if start:
+        output_string = '{0}{1}{2}'.format(
+            start, separator, output_string)[0:length]
+    return output_string
 
 
 @check_len
 @check_validation
-def gen_special(length=10):
+def gen_special(length=10, start=None, separator=''):
     """Return a random special characters string.
 
     :param int length: Length for random data.
+    :param str start: Random data start with.
+    :param char separator: Separator for start and random data.
     :returns: A random string made up of special characters.
     :rtype: str
     """
     random.seed()
-    return ''.join(random.choice(string.punctuation) for _ in range(length))
+    output_string = ''.join(
+        random.choice(string.punctuation) for _ in range(length)
+    )
+
+    if start:
+        output_string = '{0}{1}{2}'.format(
+            start, separator, output_string)[0:length]
+    return output_string


Test patch:
diff --git a/tests/test_strings.py b/tests/test_strings.py
index 7b977a3..2095fe4 100644
--- a/tests/test_strings.py
+++ b/tests/test_strings.py
@@ -1,6 +1,7 @@
 """Tests for all string generators."""
 import string
 import unicodedata
+from random import randint
 
 import pytest
 
@@ -150,3 +151,14 @@ def test_special_string():
     special_str = gen_special()
     for char in special_str:
         assert char in VALID_CHARS
+
+
+@pytest.mark.parametrize('fnc', GENERATORS[1:])
+def test_start_string(fnc):
+    """"String generated has start with specific keyword."""
+    start = fnc(randint(1, 5))
+    separator = fnc(1)
+    random_str = fnc(start=start, separator=separator)
+    assert start == random_str[0:len(start)]
+    assert separator == random_str[len(start)]
+    assert len(random_str) == 10