From 5790b90da0722eed91ab71397dc0a24f77b3dd0d Mon Sep 17 00:00:00 2001 From: Quinten Steenhuis Date: Fri, 9 Aug 2024 17:13:12 -0400 Subject: [PATCH 1/4] Fix #874 - don't trigger gender, better handling of non-disclosed pronouns --- docassemble/AssemblyLine/al_general.py | 286 +++++++++++++++---------- 1 file changed, 168 insertions(+), 118 deletions(-) diff --git a/docassemble/AssemblyLine/al_general.py b/docassemble/AssemblyLine/al_general.py index 119e020..06b95ea 100644 --- a/docassemble/AssemblyLine/al_general.py +++ b/docassemble/AssemblyLine/al_general.py @@ -1562,18 +1562,18 @@ def address_block( ) def pronoun(self, **kwargs) -> str: - """Returns an objective pronoun as appropriate, based on attributes. + """Returns an objective pronoun as appropriate, based on the user's `pronouns` attribute or `gender` attribute. - The pronoun could be "you," "her," "him," "it," or "them". It depends - on the `gender` and `person_type` attributes and whether the individual - is the current user. + The pronoun could be "I", "you," "her," "him," "it," or "them", or a user-provided pronoun. + If the user has selected multiple pronouns, each will appear, separated by a "/". - If the user selected specific pronouns, they take priority over - gender (only if they chose a pronoun from the list) + This method will not trigger the definition of `gender` or `pronouns`, but it will use them if they are defined, + with `pronouns` taking precedence. As a default, it will either use the value of `default` or the individual's full name. Args: - **kwargs: Additional keyword arguments. - + **kwargs: Additional keyword arguments that are defined [upstream](https://docassemble.org/docs/objects.html#language%20methods). + person (Optional[[Union[str,int]]): Whether to use a first, second, or third person pronoun. Can be one of 1/"1p", 2/"2p", or 3/"3p" (default is 3). See [upstream](https://docassemble.org/docs/objects.html#language%20methods) documentation for more information. + default (Optional[str]): The default word to use if the pronoun is not defined, e.g. "the agent". If not defined, the default term is the user's name. Returns: str: The appropriate pronoun. """ @@ -1583,49 +1583,66 @@ def pronoun(self, **kwargs) -> str: # Use the parent version of pronoun return super().pronoun(**kwargs) - if hasattr(self, "pronouns") and isinstance(self.pronouns, str): - pronouns = DADict(elements={self.pronouns.lower(): True}) + if "default" in kwargs: + default = kwargs.pop("default") else: - pronouns = self.pronouns + default = self.name_full() + + if hasattr(self, "pronouns"): + if isinstance(self.pronouns, str): + pronouns = DADict(elements={self.pronouns.lower(): True}) + else: + pronouns = self.pronouns if self == this_thread.global_vars.user: output = word("you", **kwargs) - elif ( - hasattr(self, "pronouns") - and isinstance(pronouns, DADict) - and len(pronouns.true_values()) == 1 - and ( - ( - pronouns.true_values()[0] - in ["she/her/hers", "he/him/his", "they/them/theirs", "ze/zir/zirs"] - ) - or ( - pronouns.get("self-described") - and has_parsable_pronouns(self.pronouns_self_described) - ) - ) - ): - if pronouns.get("she/her/hers"): - output = word("her", **kwargs) - elif pronouns.get("he/him/his"): - output = word("him", **kwargs) - elif pronouns.get("they/them/theirs"): - output = word("them", **kwargs) - elif pronouns.get("ze/zir/zirs"): - output = word("zir", **kwargs) - elif pronouns.get("self-described"): - output = parse_custom_pronouns(self.pronouns_self_described)["o"] + elif hasattr(self, "pronouns"): + if isinstance(pronouns, DADict): + pronouns_to_use = [] + for pronoun in pronouns.true_values(): + if pronoun in [ + "she/her/hers", + "he/him/his", + "they/them/theirs", + "ze/zir/zirs", + ]: + if pronoun == "she/her/hers": + pronouns_to_use.append(word("her", **kwargs)) + elif pronoun == "he/him/his": + pronouns_to_use.append(word("him", **kwargs)) + elif pronoun == "they/them/theirs": + pronouns_to_use.append(word("them", **kwargs)) + elif pronoun == "ze/zir/zirs": + pronouns_to_use.append(word("zir", **kwargs)) + elif pronoun == "self-described" and has_parsable_pronouns( + self.pronouns_self_described + ): + pronouns_to_use.append( + parse_custom_pronouns(self.pronouns_self_described)["o"] + ) + elif has_parsable_pronouns(pronoun): + pronouns_to_use.append( + parse_custom_pronouns(pronoun)["o"] + ) + if len(pronouns_to_use) > 0: + output = "/".join(pronouns_to_use) + else: + output = default elif hasattr(self, "person_type") and self.person_type in [ "business", "organization", ]: output = word("it", **kwargs) - elif self.gender.lower() == "female": - output = word("her", **kwargs) - elif self.gender.lower() == "male": - output = word("him", **kwargs) + elif hasattr(self, "gender"): + if self.gender.lower() == "female": + output = word("her", **kwargs) + elif self.gender.lower() == "male": + output = word("him", **kwargs) + else: + output = word("them", **kwargs) else: - output = word("them", **kwargs) + output = default + if "capitalize" in kwargs and kwargs["capitalize"]: return capitalize(output) return output @@ -1644,14 +1661,18 @@ def pronoun_objective(self, **kwargs) -> str: def pronoun_possessive(self, target, **kwargs) -> str: """Returns a possessive pronoun and a target word, based on attributes. + This method will not trigger the definition of `gender` or `pronouns`, but it will use them if they are defined, + with `pronouns` taking precedence. As a default, it will either use the value of `default` or the individual's full name. + Given a target word, the function returns "{pronoun} {target}". The pronoun could be - "her," "his," "its," or "their". It depends on the `gender` and `person_type` attributes + "my", "her," "his," "its," or "their". It depends on the `gender` and `person_type` attributes and whether the individual is the current user. Args: target (str): The target word to follow the pronoun. - **kwargs: Additional keyword arguments. - + person (Optional[[Union[str,int]]): Whether to use a first, second, or third person pronoun. Can be one of 1/"1p", 2/"2p", or 3/"3p" (default is 3). See [upstream](https://docassemble.org/docs/objects.html#language%20methods) documentation for more information. + default (Optional[str]): The default word to use if the pronoun is not defined, e.g. "the agent". If not defined, the default term is the user's name. + **kwargs: Additional keyword arguments that are defined [upstream](https://docassemble.org/docs/objects.html#language%20methods). Returns: str: The appropriate possessive phrase. """ @@ -1666,50 +1687,64 @@ def pronoun_possessive(self, target, **kwargs) -> str: else: pronouns = self.pronouns + if "default" in kwargs: + default = kwargs.pop("default") + else: + default = self.name_full() + if self == this_thread.global_vars.user and ( "thirdperson" not in kwargs or not kwargs["thirdperson"] ): output = your(target, **kwargs) - elif ( - hasattr(self, "pronouns") - and isinstance(pronouns, DADict) - and len(pronouns.true_values()) == 1 - and ( - ( - pronouns.true_values()[0] - in ["she/her/hers", "he/him/his", "they/them/theirs", "ze/zir/zirs"] - ) - or ( - pronouns.get("self-described") - and has_parsable_pronouns(self.pronouns_self_described) - ) - ) - ): - if pronouns.get("she/her/hers"): - output = her(target, **kwargs) - elif pronouns.get("he/him/his"): - output = his(target, **kwargs) - elif pronouns.get("they/them/theirs"): - output = their(target, **kwargs) - elif pronouns.get("ze/zir/zirs"): - output = word("zir", **kwargs) + " " + target - elif pronouns.get("self-described"): - output = ( - parse_custom_pronouns(self.pronouns_self_described)["p"] - + " " - + target - ) + elif hasattr(self, "pronouns"): + if isinstance(pronouns, DADict): + pronouns_to_use = [] + for pronoun in pronouns.true_values(): + if pronoun in [ + "she/her/hers", + "he/him/his", + "they/them/theirs", + "ze/zir/zirs", + ]: + if pronoun == "she/her/hers": + pronouns_to_use.append(her(target, **kwargs)) + elif pronoun == "he/him/his": + pronouns_to_use.append(his(target, **kwargs)) + elif pronoun == "they/them/theirs": + pronouns_to_use.append(their(target, **kwargs)) + elif pronoun == "ze/zir/zirs": + pronouns_to_use.append(word("zir", **kwargs) + " " + target) + elif pronoun == "self-described" and has_parsable_pronouns( + self.pronouns_self_described + ): + pronouns_to_use.append( + parse_custom_pronouns(self.pronouns_self_described)["p"] + + " " + + target + ) + elif has_parsable_pronouns(pronoun): + pronouns_to_use.append( + parse_custom_pronouns(pronoun)["p"] + " " + target + ) + if len(pronouns_to_use) > 0: + output = "/".join(pronouns_to_use) + else: + output = default elif hasattr(self, "person_type") and self.person_type in [ "business", "organization", ]: output = its(target, **kwargs) - elif self.gender.lower() == "female": - output = her(target, **kwargs) - elif self.gender.lower() == "male": - output = his(target, **kwargs) + elif hasattr(self, "gender"): + if self.gender.lower() == "female": + output = her(target, **kwargs) + elif self.gender.lower() == "male": + output = his(target, **kwargs) + else: + output = their(target, **kwargs) else: - output = their(target, **kwargs) + output = default + if "capitalize" in kwargs and kwargs["capitalize"]: return capitalize(output) return output @@ -1717,13 +1752,14 @@ def pronoun_possessive(self, target, **kwargs) -> str: def pronoun_subjective(self, **kwargs) -> str: """Returns a subjective pronoun, based on attributes. - The pronoun could be "you," "she," "he," "it," or "they". It depends + The pronoun could be "you," "we", "she," "he," "it," or "they". It depends on the `gender` and `person_type` attributes and whether the individual is the current user. Args: - **kwargs: Additional keyword arguments. - + **kwargs: Additional keyword arguments that are defined [upstream](https://docassemble.org/docs/objects.html#language%20methods). + person (Optional[[Union[str,int]]): Whether to use a first, second, or third person pronoun. Can be one of 1/"1p", 2/"2p", or 3/"3p" (default is 3). See [upstream](https://docassemble.org/docs/objects.html#language%20methods) documentation for more information. + default (Optional[str]): The default word to use if the pronoun is not defined, e.g. "the agent". If not defined, the default term is the user's name. Returns: str: The appropriate subjective pronoun. """ @@ -1732,52 +1768,66 @@ def pronoun_subjective(self, **kwargs) -> str: if person in ("1", "1p", "2", "2p"): # Use the parent version of pronoun return super().pronoun_subjective(**kwargs) - - if hasattr(self, "pronouns") and isinstance(self.pronouns, str): - pronouns = DADict(elements={self.pronouns.lower(): True}) + if "default" in kwargs: + default = kwargs.pop("default") else: - pronouns = self.pronouns + default = self.name_full() - if self == this_thread.global_vars.user and ( - "thirdperson" not in kwargs or not kwargs["thirdperson"] - ): + if hasattr(self, "pronouns"): + if isinstance(self.pronouns, str): + pronouns = DADict(elements={self.pronouns.lower(): True}) + else: + pronouns = self.pronouns + + if self == this_thread.global_vars.user: output = word("you", **kwargs) - elif ( - hasattr(self, "pronouns") - and isinstance(pronouns, DADict) - and len(pronouns.true_values()) == 1 - and ( - ( - pronouns.true_values()[0] - in ["she/her/hers", "he/him/his", "they/them/theirs", "ze/zir/zirs"] - ) - or ( - pronouns.get("self-described") - and has_parsable_pronouns(self.pronouns_self_described) - ) - ) - ): - if pronouns.get("she/her/hers"): - output = word("she", **kwargs) - elif pronouns.get("he/him/his"): - output = word("he", **kwargs) - elif pronouns.get("they/them/theirs"): - output = word("they", **kwargs) - elif pronouns.get("ze/zir/zirs"): - output = word("ze", **kwargs) - elif pronouns.get("self-described"): - output = parse_custom_pronouns(self.pronouns_self_described)["s"] + elif hasattr(self, "pronouns"): + if isinstance(pronouns, DADict): + pronouns_to_use = [] + for pronoun in pronouns.true_values(): + if pronoun in [ + "she/her/hers", + "he/him/his", + "they/them/theirs", + "ze/zir/zirs", + ]: + if pronoun == "she/her/hers": + pronouns_to_use.append(word("she", **kwargs)) + elif pronoun == "he/him/his": + pronouns_to_use.append(word("he", **kwargs)) + elif pronoun == "they/them/theirs": + pronouns_to_use.append(word("they", **kwargs)) + elif pronoun == "ze/zir/zirs": + pronouns_to_use.append(word("ze", **kwargs)) + elif pronoun == "self-described" and has_parsable_pronouns( + self.pronouns_self_described + ): + pronouns_to_use.append( + parse_custom_pronouns(self.pronouns_self_described)["s"] + ) + elif has_parsable_pronouns(pronoun): + pronouns_to_use.append( + parse_custom_pronouns(pronoun)["s"] + ) + if len(pronouns_to_use) > 0: + output = "/".join(pronouns_to_use) + else: + output = default elif hasattr(self, "person_type") and self.person_type in [ "business", "organization", ]: output = word("it", **kwargs) - elif self.gender.lower() == "female": - output = word("she", **kwargs) - elif self.gender.lower() == "male": - output = word("he", **kwargs) + elif hasattr(self, "gender"): + if self.gender.lower() == "female": + output = word("she", **kwargs) + elif self.gender.lower() == "male": + output = word("he", **kwargs) + else: + output = word("they", **kwargs) else: - output = word("they", **kwargs) + output = default + if "capitalize" in kwargs and kwargs["capitalize"]: return capitalize(output) return output From 5e7d52f435ff9e10e00cb617f1860d9cc9822184 Mon Sep 17 00:00:00 2001 From: Quinten Steenhuis Date: Fri, 9 Aug 2024 17:30:35 -0400 Subject: [PATCH 2/4] Fixes from unit test failures --- docassemble/AssemblyLine/al_general.py | 18 +++++++++--------- docassemble/AssemblyLine/test_al_general.py | 6 +++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docassemble/AssemblyLine/al_general.py b/docassemble/AssemblyLine/al_general.py index 06b95ea..06dd00b 100644 --- a/docassemble/AssemblyLine/al_general.py +++ b/docassemble/AssemblyLine/al_general.py @@ -1588,7 +1588,7 @@ def pronoun(self, **kwargs) -> str: else: default = self.name_full() - if hasattr(self, "pronouns"): + if hasattr(self, "pronouns") and self.pronouns: if isinstance(self.pronouns, str): pronouns = DADict(elements={self.pronouns.lower(): True}) else: @@ -1596,9 +1596,9 @@ def pronoun(self, **kwargs) -> str: if self == this_thread.global_vars.user: output = word("you", **kwargs) - elif hasattr(self, "pronouns"): + elif hasattr(self, "pronouns") and self.pronouns: + pronouns_to_use = [] if isinstance(pronouns, DADict): - pronouns_to_use = [] for pronoun in pronouns.true_values(): if pronoun in [ "she/her/hers", @@ -1696,9 +1696,9 @@ def pronoun_possessive(self, target, **kwargs) -> str: "thirdperson" not in kwargs or not kwargs["thirdperson"] ): output = your(target, **kwargs) - elif hasattr(self, "pronouns"): - if isinstance(pronouns, DADict): - pronouns_to_use = [] + elif hasattr(self, "pronouns") and self.pronouns: + pronouns_to_use = [] + if isinstance(pronouns, DADict): for pronoun in pronouns.true_values(): if pronoun in [ "she/her/hers", @@ -1773,7 +1773,7 @@ def pronoun_subjective(self, **kwargs) -> str: else: default = self.name_full() - if hasattr(self, "pronouns"): + if hasattr(self, "pronouns") and self.pronouns: if isinstance(self.pronouns, str): pronouns = DADict(elements={self.pronouns.lower(): True}) else: @@ -1781,9 +1781,9 @@ def pronoun_subjective(self, **kwargs) -> str: if self == this_thread.global_vars.user: output = word("you", **kwargs) - elif hasattr(self, "pronouns"): + elif hasattr(self, "pronouns") and self.pronouns: + pronouns_to_use = [] if isinstance(pronouns, DADict): - pronouns_to_use = [] for pronoun in pronouns.true_values(): if pronoun in [ "she/her/hers", diff --git a/docassemble/AssemblyLine/test_al_general.py b/docassemble/AssemblyLine/test_al_general.py index d2bd1e7..a65fd6b 100644 --- a/docassemble/AssemblyLine/test_al_general.py +++ b/docassemble/AssemblyLine/test_al_general.py @@ -61,6 +61,8 @@ def setUp(self): # Assigning this_thread to self.individual self.individual.this_thread = self.this_thread + self.individual.name.first = "John" + def test_phone_numbers(self): self.assertEqual(self.individual.phone_numbers(), "") self.individual.phone_number = "" @@ -423,9 +425,7 @@ def test_custom_pronouns(self): self.assertEqual(self.individual.pronoun_possessive("fish"), "xem fish") self.individual.pronouns_self_described = "Xe/Xir/Xirs/xem/xirself" - # Should raise an exception - with self.assertRaises(DAAttributeError): - self.individual.pronoun_objective() + self.assertEqual(self.individual.pronoun_objective(), "John") def test_name_methods(self): self.individual.name.first = "John" From 080d905699ec846abc26a5c6b4d095625e737fb9 Mon Sep 17 00:00:00 2001 From: Quinten Steenhuis Date: Fri, 9 Aug 2024 17:35:00 -0400 Subject: [PATCH 3/4] Formatting with black --- docassemble/AssemblyLine/al_general.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/docassemble/AssemblyLine/al_general.py b/docassemble/AssemblyLine/al_general.py index 06dd00b..b538547 100644 --- a/docassemble/AssemblyLine/al_general.py +++ b/docassemble/AssemblyLine/al_general.py @@ -1621,9 +1621,7 @@ def pronoun(self, **kwargs) -> str: parse_custom_pronouns(self.pronouns_self_described)["o"] ) elif has_parsable_pronouns(pronoun): - pronouns_to_use.append( - parse_custom_pronouns(pronoun)["o"] - ) + pronouns_to_use.append(parse_custom_pronouns(pronoun)["o"]) if len(pronouns_to_use) > 0: output = "/".join(pronouns_to_use) else: @@ -1698,7 +1696,7 @@ def pronoun_possessive(self, target, **kwargs) -> str: output = your(target, **kwargs) elif hasattr(self, "pronouns") and self.pronouns: pronouns_to_use = [] - if isinstance(pronouns, DADict): + if isinstance(pronouns, DADict): for pronoun in pronouns.true_values(): if pronoun in [ "she/her/hers", @@ -1806,9 +1804,7 @@ def pronoun_subjective(self, **kwargs) -> str: parse_custom_pronouns(self.pronouns_self_described)["s"] ) elif has_parsable_pronouns(pronoun): - pronouns_to_use.append( - parse_custom_pronouns(pronoun)["s"] - ) + pronouns_to_use.append(parse_custom_pronouns(pronoun)["s"]) if len(pronouns_to_use) > 0: output = "/".join(pronouns_to_use) else: From b0c90e24024ba0a7405e58016505104bb91d1798 Mon Sep 17 00:00:00 2001 From: Quinten Steenhuis Date: Fri, 9 Aug 2024 19:11:50 -0400 Subject: [PATCH 4/4] Changes to docstrings for docsig --- docassemble/AssemblyLine/al_general.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/docassemble/AssemblyLine/al_general.py b/docassemble/AssemblyLine/al_general.py index b538547..a583429 100644 --- a/docassemble/AssemblyLine/al_general.py +++ b/docassemble/AssemblyLine/al_general.py @@ -1572,8 +1572,8 @@ def pronoun(self, **kwargs) -> str: Args: **kwargs: Additional keyword arguments that are defined [upstream](https://docassemble.org/docs/objects.html#language%20methods). - person (Optional[[Union[str,int]]): Whether to use a first, second, or third person pronoun. Can be one of 1/"1p", 2/"2p", or 3/"3p" (default is 3). See [upstream](https://docassemble.org/docs/objects.html#language%20methods) documentation for more information. - default (Optional[str]): The default word to use if the pronoun is not defined, e.g. "the agent". If not defined, the default term is the user's name. + - person (Optional[[Union[str,int]]): Whether to use a first, second, or third person pronoun. Can be one of 1/"1p", 2/"2p", or 3/"3p" (default is 3). See [upstream](https://docassemble.org/docs/objects.html#language%20methods) documentation for more information. + - default (Optional[str]): The default word to use if the pronoun is not defined, e.g. "the agent". If not defined, the default term is the user's name. Returns: str: The appropriate pronoun. """ @@ -1657,7 +1657,8 @@ def pronoun_objective(self, **kwargs) -> str: return self.pronoun(**kwargs) def pronoun_possessive(self, target, **kwargs) -> str: - """Returns a possessive pronoun and a target word, based on attributes. + """ + Returns a possessive pronoun and a target word, based on attributes. This method will not trigger the definition of `gender` or `pronouns`, but it will use them if they are defined, with `pronouns` taking precedence. As a default, it will either use the value of `default` or the individual's full name. @@ -1668,11 +1669,12 @@ def pronoun_possessive(self, target, **kwargs) -> str: Args: target (str): The target word to follow the pronoun. - person (Optional[[Union[str,int]]): Whether to use a first, second, or third person pronoun. Can be one of 1/"1p", 2/"2p", or 3/"3p" (default is 3). See [upstream](https://docassemble.org/docs/objects.html#language%20methods) documentation for more information. - default (Optional[str]): The default word to use if the pronoun is not defined, e.g. "the agent". If not defined, the default term is the user's name. - **kwargs: Additional keyword arguments that are defined [upstream](https://docassemble.org/docs/objects.html#language%20methods). + **kwargs: Additional keyword arguments that can be passed to modify the behavior. These might include: + - `default` (Optional[str]): The default word to use if the pronoun is not defined, e.g., "the agent". If not defined, the default term is the user's name. + - `person` (Optional[Union[str, int]]): Whether to use a first, second, or third person pronoun. Can be one of 1/"1p", 2/"2p", or 3/"3p" (default is 3). See [upstream documentation](https://docassemble.org/docs/objects.html#language%20methods) for more information. + Returns: - str: The appropriate possessive phrase. + str: The appropriate possessive phrase, e.g., "her book", "their document". """ person = str(kwargs.get("person", self.get_point_of_view())) @@ -1756,8 +1758,8 @@ def pronoun_subjective(self, **kwargs) -> str: Args: **kwargs: Additional keyword arguments that are defined [upstream](https://docassemble.org/docs/objects.html#language%20methods). - person (Optional[[Union[str,int]]): Whether to use a first, second, or third person pronoun. Can be one of 1/"1p", 2/"2p", or 3/"3p" (default is 3). See [upstream](https://docassemble.org/docs/objects.html#language%20methods) documentation for more information. - default (Optional[str]): The default word to use if the pronoun is not defined, e.g. "the agent". If not defined, the default term is the user's name. + - person (Optional[[Union[str,int]]): Whether to use a first, second, or third person pronoun. Can be one of 1/"1p", 2/"2p", or 3/"3p" (default is 3). See [upstream](https://docassemble.org/docs/objects.html#language%20methods) documentation for more information. + - default (Optional[str]): The default word to use if the pronoun is not defined, e.g. "the agent". If not defined, the default term is the user's name. Returns: str: The appropriate subjective pronoun. """